Learning Paths & Tutorials

Zero to Production

Four phases from in-memory to live.

This guide is a practical roadmap. Each phase has clear goals, code examples, and a checklist before you move on.

PURISTA is designed so each phase is independently deployable. You can ship at Phase 1 (a working monolith with tests) and add stores, brokers, and observability incrementally. There is no point where you must stop and rewrite. Each phase builds on the last without changing the service code you already wrote.

flowchart LR
    p1["**Phase 1**<br/>Foundation"] --> p2["**Phase 2**<br/>Integration"] --> p3["**Phase 3**<br/>Runtime"] --> p4["**Phase 4**<br/>Production"]
    style p1 fill:var(--color-con),stroke:none,color:#fff
    style p2 fill:var(--color-found),stroke:none,color:#fff
    style p3 fill:var(--color-pilot),stroke:none,color:#fff
    style p4 fill:var(--color-con),stroke:none,color:#fff

Phase 1: Foundation

Goal: A running service with commands, subscriptions, and tests — using only the in-memory event bridge.

What to build

  1. Scaffold a project with npm create purista@latest
  2. Create a service with at least one command
  3. Add one subscription reacting to a command event
  4. Define Zod schemas for all inputs and outputs
  5. Write unit tests for the service, command, and subscription

Example: service bootstrap

import { DefaultEventBridge } from '@purista/core'
import { userServiceV1Service } from './service/user/v1/userServiceV1Service.js'

const eventBridge = new DefaultEventBridge()
await eventBridge.start()

const userService = await userServiceV1Service.getInstance(eventBridge)
await userService.start()

console.log('Service is running. Press Ctrl+C to stop.')

Example: first command test

import { createCommandTestHarness } from '@purista/core'
import { userV1ServiceBuilder } from './userV1ServiceBuilder.js'
import { userSignUpCommandBuilder } from './userSignUpCommandBuilder.js'

test('creates a user and returns an id', async () => {
  const harness = await createCommandTestHarness(userV1ServiceBuilder, userSignUpCommandBuilder, {
    resources: { db: { createUser: async () => 'user-123' } },
  })

  const result = await harness.run({
    payload: { email: 'test@example.com', password: 'secure-password' },
    parameter: {},
  })

  expect(result.userId).toBe('user-123')
})

Phase 1 checklist

  • Project scaffolded with purista.json configured
  • At least one service with serviceInfo typed
  • Commands have .addPayloadSchema() and .addOutputSchema()
  • Subscriptions have explicit filters
  • Unit tests pass for all builders
  • No any or unknown types in core message paths

Phase 2: Integration-ready

Goal: Your services can persist state, call external APIs, and expose HTTP endpoints.

What to add

  1. Config, secret, and state stores — externalize all mutable state
  2. Resources — database clients, HTTP clients, SDK wrappers
  3. HTTP exposure — expose commands via REST
  4. Invoke relations — explicitly declare cross-service calls

Example: adding stores and resources

Resources are declared on the builder as types, then injected at instantiation:

import { ServiceBuilder } from '@purista/core'
import type { DbClient } from './db.js'

export const userServiceV1ServiceBuilder = new ServiceBuilder(myServiceInfo)
  .defineResource<'db', DbClient>()
import { RedisStateStore } from '@purista/redis-state-store'

const userService = await userServiceV1Service.getInstance(eventBridge, {
  stateStore: new RedisStateStore({ url: process.env.REDIS_URL }),
  resources: { db: createDbClient(process.env.DATABASE_URL) },
})

Example: exposing a command as HTTP

export const userSignUpCommandBuilder = userServiceV1ServiceBuilder
  .getCommandBuilder('userSignUp', 'Register a new user')
  .exposeAsHttpEndpoint('POST', 'users')
  .setCommandFunction(async function (context, payload) {
    // business logic
  })

Phase 2 checklist

  • Config stores for environment-specific values
  • Secret stores for API keys, DB passwords
  • State stores for business state
  • Resources declared in service builder
  • At least one command exposed via .exposeAsHttpEndpoint()
  • canInvoke declarations for cross-service calls
  • Error handling tested for external service failures

Phase 3: Runtime architecture

Goal: Choose the infrastructure that matches your delivery and scaling requirements.

Decisions to make

DecisionOptionsWhen to choose
Event bridgeDefault, AMQP, NATS, DaprSee Event Bridges comparison
Queue bridgeDefault, Redis, NATS JetStreamUse Redis/NATS for production pull-based workloads
Deployment modelMonolith, microservice, serverlessSee Deployment
HTTP server@purista/hono-http-server, customHono adapter is recommended

Example: switching to AMQP

import { AmqpBridge } from '@purista/amqpbridge'

const eventBridge = new AmqpBridge({
  url: process.env.AMQP_URL,
  exchangeName: 'purista',
})

Your service code does not change. Only the bootstrap file changes.

Phase 3 checklist

  • Event bridge chosen and configured for target environment
  • Queue bridge configured if using pull-based workloads
  • Deployment model documented
  • Graceful shutdown and startup ordering implemented
  • Health checks configured
  • Integration tests run against real broker/store setup

Phase 4: Production readiness

Goal: The system is observable, secure, and resilient.

What to enable

  1. OpenTelemetry — traces, metrics, structured logs
  2. Authentication / authorization — protect HTTP endpoints
  3. Error handling — validate timeout behavior, retry policies
  4. Integration tests — test against real infrastructure

Example: enabling OpenTelemetry

PURISTA does not use NodeSDK. Pass a SpanProcessor directly to the event bridge and each service:

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'

const spanProcessor = new SimpleSpanProcessor(
  new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT })
)

const eventBridge = new AmqpBridge({ url: process.env.AMQP_URL, spanProcessor })
const userService = await userServiceV1Service.getInstance(eventBridge, { spanProcessor })

Example: endpoint protection

export const userSignUpCommandBuilder = userServiceV1ServiceBuilder
  .getCommandBuilder('userSignUp', 'Register a new user')
  .exposeAsHttpEndpoint('POST', 'users')
  .makeEndpointSecured()

Phase 4 checklist

  • OpenTelemetry exporter configured and verified
  • Auth middleware applied to protected endpoints
  • Error handling tested: happy path, failure path, timeout
  • Retry policies defined
  • Integration tests pass against staging infrastructure
  • Observability dashboards reviewed
  • Runtime config documented

Pre-launch checklist

AreaCheck
SchemasAll inputs and outputs have explicit Zod schemas
TypesNo any / unknown in core message paths
TestsUnit tests cover happy and failure paths
ConfigRuntime config is documented and version-controlled
ObservabilityTraces, logs, and metrics are visible
SecuritySecrets are in secret stores, not env vars
ShutdownGraceful shutdown behavior verified

Summary

PhaseTime estimateKey deliverable
1. FoundationHoursRunning service with tests
2. Integration1–2 daysHTTP endpoints, stores, resources
3. Runtime1–2 daysBroker chosen, deployment model defined
4. Production2–3 daysObservability, auth, integration tests

Related

Read Next
Philosophy

from Mental Model & Philosophy