HonoModules, or HonoExtensions, and a deployment composes them.
This page covers the transport, the surface split, the authoring conventions, the -contracts packages that pin the wire types, and how route ownership and composition work.
The Hono transport
@voyant-travel/hono provides createApp(), the middleware chain, auth helpers, and plugin expansion. A deployment builds its app by handing createApp its database client, auth integration, and the set of modules, extensions, and plugins it wants mounted.
createApp runs a fixed middleware chain before any module route sees the request:
c.var.db, and the shared error boundary wrapping it. That is why route handlers can stay short: the cross-cutting work is done.
The surface split
Voyant has three transport buckets, and every route belongs to exactly one of them.| Surface | Purpose |
|---|---|
/v1/admin/* | Staff and operator routes: back-office CRUD, internal tooling, workflow operations. |
/v1/public/* | Customer, partner, supplier, and other external-facing contracts. |
/auth/* | Session and authenticated-user operations. |
HonoModule exposes adminRoutes and publicRoutes. createApp mounts them under /v1/admin/{name} and /v1/public/{name}. So bookingsModule lands at /v1/admin/bookings and /v1/public/bookings without the package hard-coding those prefixes.
storefront is a package and runtime concept (@voyant-travel/storefront, @voyant-travel/storefront-react), not an HTTP namespace. The customer-facing API stays under /v1/public/*, not /v1/public/storefront/*. Do not add a redundant frontend-named URL layer.publicPath when the package name would add redundant nesting or when the contract is genuinely root-scoped. Use it sparingly; the default is {module.name}. Admin routes deliberately do not get a symmetric adminPath: admin URLs stay tied to the module name so permission derivation, transactional routing, and operator debuggability stay predictable.
A small route example
A well-formed route validates input, leans on the shared auth pipeline, calls a service, and serializes an intentional response.Request validation
Two shared helpers keep parsing consistent across every package:parseJsonBody(c, schema)for JSON request bodies. It gives every route the same invalid-JSON handling, the same schema-validation responses, and the same error codes and payload shape. Prefer it over open-codedawait c.req.json().parseQuery(c, schema)for query strings. Prefer it over manualnew URL(...).searchParamsplus ad hoc coercion.
Error handling
Routes use the shared API error model rather than hand-crafting JSON for common error classes. The error boundary middleware serializes a consistent envelope, and shared error types cover the usual cases:RequestValidationErrorUnauthorizedApiErrorForbiddenApiError
handleApiError(...) rather than a hand-built payload. The point is that admin and public responses share one error contract: each package does not invent its own 400, 401, or 403 shape. See Errors and transport for how clients consume that envelope.
The contracts packages
A module’s wire shape is not an accident of its current service internals. It lives in a dedicated-contracts package, for example @voyant-travel/bookings-contracts. That package describes the request and response types shared between the server implementation and the client SDK, so there is one source of truth for the API surface.
This is the contract-first idea that runs through the whole framework:
- The route handler validates and serializes against the contract.
- The
-reactpackage consumes the same contract to give you typed hooks instead of hand-written fetch calls. - The public SDKs are typed clients generated against that stable surface.
Route ownership
Reusable route interfaces belong with the thing that owns the capability, not with the deployment. The ownership rules are straightforward:Modules own capability routes
If a package defines a capability with its own records and behavior, the module owns its admin CRUD, public contracts, route schemas, and route tests.
Extensions own behavior around a module
A route that customizes another module without introducing a new bounded capability is a
HonoExtension. Example: finance booking-tax routes mounted under the bookings surface.Adapters own vendor routes
A package that exists to talk to an external system owns its vendor-specific routes. The deployment supplies credentials and base URLs; the adapter owns the handlers.
Deployments own composition
Starters own DB selection, auth integration, provider credentials, deployment URLs, and genuinely app-specific routes, then compose everything together.
How an app composes module routes
The operator starter composes its surface from a manifest plus a typed capability container, usingcomposeFromManifest(...) from @voyant-travel/hono/composition. The shape co-locates each runtime unit’s package specifier with its factory and a typed option builder, then derives both the manifest and the registry from that one list.
Lazy route contributions
A heavy route family (flights, media, catalog offers, document generation) can be loaded on demand.@voyant-travel/hono exposes lazyAdminRoutes / lazyPublicRoutes: a loader () => Promise<Hono> mounted at the surface prefix, dynamically imported on the first matching request and cached per isolate. The framework bridges the request context across the lazy forward, so the loaded sub-app still sees c.var.db, the container, and the resolved actor. A deployment gets the heavy family just by composing the module; there is no starter wiring and no cold-start penalty for routes that are never hit.
Authoring checklist
When you author or review a route:Validate through the shared helpers
parseJsonBody(...) for bodies, parseQuery(...) for query strings.Use the smallest auth guard
requireUserId(...) for signed-in, actor-aware guards for workspace context, permission checks for explicit grants. See Auth.REST reference
Every route the framework modules expose is documented as structured REST reference, generated from the route handlers and grouped by module. See the Admin API and Storefront API sections in the Framework navigation:- Admin API covers the staff-facing
/v1/admin/*endpoints. - Storefront API covers the customer-facing
/v1/public/*endpoints.
Next steps
Services
The business logic routes delegate to.
Auth
Actor types, the auth middleware, API tokens, and scopes.
Caching
What is cacheable on the public surface and how invalidation works.
SDK authentication
How typed clients authenticate against these routes.