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
- Scaffold a project with
npm create purista@latest - Create a service with at least one command
- Add one subscription reacting to a command event
- Define Zod schemas for all inputs and outputs
- 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.jsonconfigured - At least one service with
serviceInfotyped - Commands have
.addPayloadSchema()and.addOutputSchema() - Subscriptions have explicit filters
- Unit tests pass for all builders
- No
anyorunknowntypes in core message paths
Phase 2: Integration-ready
Goal: Your services can persist state, call external APIs, and expose HTTP endpoints.
What to add
- Config, secret, and state stores — externalize all mutable state
- Resources — database clients, HTTP clients, SDK wrappers
- HTTP exposure — expose commands via REST
- 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() -
canInvokedeclarations 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
| Decision | Options | When to choose |
|---|---|---|
| Event bridge | Default, AMQP, NATS, Dapr | See Event Bridges comparison |
| Queue bridge | Default, Redis, NATS JetStream | Use Redis/NATS for production pull-based workloads |
| Deployment model | Monolith, microservice, serverless | See Deployment |
| HTTP server | @purista/hono-http-server, custom | Hono 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
- OpenTelemetry — traces, metrics, structured logs
- Authentication / authorization — protect HTTP endpoints
- Error handling — validate timeout behavior, retry policies
- 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
| Area | Check |
|---|---|
| Schemas | All inputs and outputs have explicit Zod schemas |
| Types | No any / unknown in core message paths |
| Tests | Unit tests cover happy and failure paths |
| Config | Runtime config is documented and version-controlled |
| Observability | Traces, logs, and metrics are visible |
| Security | Secrets are in secret stores, not env vars |
| Shutdown | Graceful shutdown behavior verified |
Summary
| Phase | Time estimate | Key deliverable |
|---|---|---|
| 1. Foundation | Hours | Running service with tests |
| 2. Integration | 1–2 days | HTTP endpoints, stores, resources |
| 3. Runtime | 1–2 days | Broker chosen, deployment model defined |
| 4. Production | 2–3 days | Observability, auth, integration tests |