RPC Router
makeRpcRouter() is the only legal way to expose non-entity HTTP routes
on a microservice. It is the closed-by-design counterpart of the BE's
makeEntityRouter: the mandatory middleware chain (auth → RBAC → body
validation → handler → RFC 7807 errors) is baked in and cannot be bypassed
by the consumer.
createMicroservice enforces this at startup: a routeHandler that was not
produced by an approved factory (makeRpcRouter, makeOpenApiHandler,
composeRouteHandlers) rejects the boot with an explicit error. The check
is a runtime brand (Symbol.for), not just TypeScript types.
Usage
Code
RpcRoute fields
| Field | Type | Purpose |
|---|---|---|
method | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | HTTP method. A matched path with the wrong method → 405. |
path | string | :param segments supported. A trailing :name* wildcard captures zero or more remaining segments (/webhook/:code/:intent*). |
auth | "user" | "api_key" | "public" | Default "user" (gateway-resolved). "api_key" requires apiKeyPort in router deps. "public" is the only kind allowed to omit permissions. |
permissions | readonly string[] | Mandatory for non-public routes — construction throws otherwise. Sentinels Permission.AUTHENTICATED_USER / AUTHENTICATED_ADMIN / PUBLIC are available. |
rbacMode | "any" | "all" | any = at least one permission (default). all = every permission. |
body | (raw: unknown) => TBody | Body validator — receives parsed JSON, throws → 400 VALIDATION_ERROR. |
rawBody | boolean | Read the body as raw text instead of parsing JSON — for pass-through routes (webhook ingress) that forward the payload verbatim. |
streaming | "sse" | Governed SSE kind: the handler gets ctx.sse and owns the response. The writer is lazy — headers flush on first send, so res.setHeader still works beforehand. |
openapi | RpcRouteOpenApi | OpenAPI metadata consumed by rpcSpec()/buildOpenApiSpec() — declare it on the route and the spec never drifts. |
handler | (ctx) => Promise<unknown> | The ONE service call. Return value → 200 JSON; write res yourself for other statuses. |
RpcRouteContext
| Field | Content |
|---|---|
user | AuthUser — undefined only on auth: "public" routes. user.id, user.isAdmin, user.permissions. |
params | Path params (wildcards may be undefined when no segments matched). |
query | URLSearchParams |
body | Output of the body validator (or raw text with rawBody). |
req / res | Raw Node objects — for custom statuses/headers. If res ends the response the router skips JSON serialization. |
sse | SseWriter — only on streaming: "sse" routes. |
Router deps
Code
Errors
The chain maps errors to RFC 7807 automatically: AuthError → 401,
RbacDeniedError → 403 RBAC_PERMISSION_DENIED, validation → 400
VALIDATION_ERROR. Everything else propagates to createHttpServer's
mapDalError/generic 500 mapping. Once headers are sent (SSE), a thrown
error destroys the socket.
Composing handlers
composeRouteHandlers(a, b, ...) chains factory-produced handlers in order —
each returns true when it handled the request. It throws at construction
time if any argument is not factory-produced: composition is an extension of
the closed set, not an escape hatch.
Enforcement rules
- Every non-
publicroute must declarepermissions— construction fails otherwise. createMicroservicecrashes at startup ifrouteHandlerlacks the factory brand.composeRouteHandlersthrows if any input is not branded.- There is no way to mount an arbitrary
(req, res, url) => boolean— migrate the endpoint to anRpcRoutedeclaration.
Next steps
-
Entity Router — the CRUD route factory built on these primitives
-
OpenAPI Generation —
rpcSpec/buildOpenApiSpecturn this route table into the service spec -
Docs Pack — the spec (and user docs) travel to the AI KB in the service pack
-
HTTP Server — the underlying server, health endpoint, RFC 7807
-
Authentication — auth kinds and internal codes
-
Service Registration — the full
createMicroservicelifecycle