Skip to main content
Ship has two layers of middleware, and they’re the same primitive — an oRPC middleware that either refines context or throws before the handler runs:
  • Global middlewares — applied to every endpoint automatically. Cross-cutting concerns: logging, timing, request context.
  • Per-endpoint gates — opt-in with .use(...): isAuthorized, isAdmin, canAccess, canEdit. Covered in Routing → Middlewares.

Global middlewares

Every endpoint builds on the @/endpoint builder, which applies the whole @/middlewares/global list — in order — before any per-endpoint .use(...):
middlewares/global/index.ts
endpoint.ts reduces that list onto the oRPC base, so adding an always-on middleware is a one-line change — register it in @/middlewares/global and every endpoint picks it up. Nothing in endpoint.ts changes:
endpoint.ts
Global middlewares must be context-preserving — cross-cutting side effects only. Anything that narrows the context (auth, ownership) is a gate you stack per-endpoint, so its type narrowing reaches the handler.

logger

The default global middleware binds a request-scoped logger. It derives the endpoint name from the oRPC path and runs the rest of the request inside an AsyncLocalStorage scope, so every log line downstream is tagged automatically:
middlewares/global/logger.ts

Adding a global middleware

Write a context-preserving oRPC middleware, then register it:
middlewares/global/timing.ts
middlewares/global/index.ts

Where the context comes from

Global middlewares don’t build the context — Hono does, before oRPC takes over. server.ts constructs an ORPCContext per request (cookies, headers, the resolved better-auth session) and hands it to the oRPC handler. By the time logger runs, context.user is already populated for signed-in requests. See Routing overview.

Validation and errors

You don’t reach for a validate middleware — validation is the endpoint. .input(zodSchema) validates the request and .output(zodSchema) shapes the response; a parse failure surfaces as a BAD_REQUEST with field-level errors. Throw ORPCError (or a ClientError / AppError from @/types) for everything else; server.ts normalises them into the response. See Routing → Middlewares for the gate-level errors (UNAUTHORIZED, FORBIDDEN, NOT_FOUND).

See also