Skip to content

Bookstore

Location: examples/bookstore

A multi-service demo showing the full contract-driven development pattern. Two services (order-service and catalog-service) communicate through events, commands, and RPC, with a runner that drives the demo flow.

  • defineDomain — Shared Zod-validated contracts are the single source of truth for message schemas. Both services import the same contract objects from @bookstore/contracts.
  • Automatic topology derivation — Each service declares its handlers and publishes in ServiceConfig. Hoppity derives all exchanges, queues, bindings, publications, and subscriptions automatically. No topology files.
  • Typed handlersonEvent, onCommand, and onRpc bind handlers to contracts. Payload types are inferred from the Zod schemas at compile time.
  • Typed broker methodsbroker.publishEvent(), broker.sendCommand(), and broker.request() enforce payload types from the contracts.
  • Event vs. command vs. RPC — The example demonstrates when to use each: events for broadcasting facts (orderCreated, orderCancelled), commands for directed work (cancelOrder), RPC for synchronous queries (createOrder, getOrderSummary, getStockLevels).
examples/bookstore/
├── packages/
│   ├── bookstore-contracts/   # Shared domain contracts (Zod schemas)
│   ├── order-service/         # Handles RPCs, handles commands, publishes events
│   └── catalog-service/       # Reacts to events, responds to RPCs
└── runner/                    # Orchestrates the demo flow

Declares RPC handlers (createOrder, getOrderSummary), a command handler (cancelOrder), and publishes two events:

const broker = await hoppity
    .service("order-service", {
        connection: { url: config.rabbitmq.url, vhost: config.rabbitmq.vhost },
        handlers: [createOrderHandler, getOrderSummaryHandler, cancelOrderHandler],
        publishes: [OrdersDomain.events.orderCreated, OrdersDomain.events.orderCancelled],
        logger,
    })
    .build();

Reacts to order events and responds to stock level queries. Only declares handlers — no explicit publishes needed because it doesn’t produce messages beyond RPC responses:

const broker = await hoppity
    .service("catalog-service", {
        connection: { url: config.rabbitmq.url, vhost: config.rabbitmq.vhost },
        handlers: [onOrderCreatedHandler, onOrderCancelledHandler, getStockLevelsHandler],
        logger,
    })
    .build();

The bookstore-contracts package defines two domains:

OrdersDomain — Events (orderCreated, orderCancelled), commands (cancelOrder), and RPCs (createOrder, getOrderSummary).

CatalogDomain — A single RPC (getStockLevels) for stock visibility.

Both services import these contracts. The contracts define the schemas once — type safety flows from contract definition through to handler payloads and broker method arguments.

cd examples/bookstore
pnpm --filter @bookstore/runner start

The runner starts both services, then drives a demo flow:

  1. Create & query — Creates an order via RPC, queries its status
  2. Cancel & query — Cancels the order via command, queries again to confirm the updated status
  • Contracts in bookstore-contracts define the schema once — both services import and use the same contract objects
  • Hoppity derives different topology for each service based on its handler and publish declarations
  • Handler payloads are fully typed from the Zod schemas — no casts, no any
  • RPC handlers in order-service publish events as side effects (e.g., createOrder publishes orderCreated)
  • Zod validation catches schema mismatches at runtime with clear error messages