> ## Documentation Index
> Fetch the complete documentation index at: https://ship.paralect.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Middlewares

> Global middlewares run on every endpoint; per-endpoint gates run on the ones that opt in. Both are oRPC middleware.

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](/docs/api-reference/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(...)`:

```ts middlewares/global/index.ts theme={null}
import logger from '@/middlewares/global/logger';

// Applied to every endpoint, in order, before any per-endpoint `.use(...)`.
// Must be context-preserving (cross-cutting side effects: logging, timing, request-id).
const globalMiddlewares = [logger];

export default globalMiddlewares;
```

`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:

```ts endpoint.ts theme={null}
import { os } from '@orpc/server';

import globalMiddlewares from '@/middlewares/global';
import type { ORPCContext } from '@/types';

const base = os.$context<ORPCContext>();

export default globalMiddlewares.reduce(
  (builder, middleware) => builder.use(middleware),
  base,
);
```

<Note>
  Global middlewares must be **context-preserving** — cross-cutting side effects only. Anything that narrows the context (auth, ownership) is a [gate](/docs/api-reference/routing/middlewares) you stack per-endpoint, so its type narrowing reaches the handler.
</Note>

### `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:

```ts middlewares/global/logger.ts theme={null}
export default base.middleware(async ({ context, path, next }) => {
  const endpoint = path.at(-1) ?? 'unknown';
  const child = appLogger.child({ endpoint });
  return loggerStorage.run(child, () => next({ context }));
});
```

## Adding a global middleware

Write a context-preserving oRPC middleware, then register it:

```ts middlewares/global/timing.ts theme={null}
import { os } from '@orpc/server';
import type { ORPCContext } from '@/types';

const base = os.$context<ORPCContext>();

export default base.middleware(async ({ context, path, next }) => {
  const start = performance.now();
  const result = await next({ context });
  appLogger.info(`${path.at(-1)} took ${Math.round(performance.now() - start)}ms`);
  return result;
});
```

```ts middlewares/global/index.ts theme={null}
import logger from '@/middlewares/global/logger';
import timing from '@/middlewares/global/timing';

const globalMiddlewares = [logger, timing];

export default globalMiddlewares;
```

## 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](/docs/api-reference/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](/docs/api-reference/routing/middlewares) for the gate-level errors (`UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`).

## See also

* [Routing → Middlewares](/docs/api-reference/routing/middlewares) — per-endpoint gate composition
* [Routing overview](/docs/api-reference/routing/overview) — how endpoints mount and the contract is generated
* [API overview](/docs/api-reference/overview) — the full Hono + oRPC story
