---
name: middlewares
description: Create and compose tRPC middleware with t.procedure.use(), extend context via opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(), define base procedures like adminProcedure and protectedProcedure. Read unvalidated input with opts.getRawInput() before .input(), check result.ok from opts.next(). Authorization, logging and timing patterns.
requires:
  - server-setup
sources:
  - www/v11/docs/server/middlewares.md
metadata:
  type: core
  library: trpc
  library_version: 11.25.0
---

<!-- Generated from www/v11/docs/server/middlewares.md by www/build/skills.ts. Edit the docs instead. -->

# Middlewares

From https://trpc.io/docs/server/middlewares

You can add middleware(s) to a procedure with the `t.procedure.use()` method. The middleware(s) will wrap the invocation of the procedure and must call `opts.next()` and return its result.

## Authorization

In the example below, any call to a `adminProcedure` will ensure that the user is an "admin" before executing.

```ts no-check
import { TRPCError, initTRPC } from '@trpc/server';

interface Context {
  user?: {
    id: string;
    isAdmin: boolean;
    // [..]
  };
}

const t = initTRPC.context<Context>().create();
export const publicProcedure = t.procedure;
export const router = t.router;

export const adminProcedure = publicProcedure.use(async (opts) => {
  const { ctx } = opts;
  if (!ctx.user?.isAdmin) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return opts.next({
    ctx: {
      user: ctx.user,
    },
  });
});
```

```ts no-check
import { adminProcedure, publicProcedure, router } from './trpc';

const adminRouter = router({
  secretPlace: adminProcedure.query(() => 'a key'),
});

export const appRouter = router({
  foo: publicProcedure.query(() => 'bar'),
  admin: adminRouter,
});
```

> [!TIP]
> See [Error Handling](https://trpc.io/docs/server/error-handling) to learn more about the `TRPCError` thrown in the above example.


## Logging

In the example below timings for queries are logged automatically.

```ts no-check
export const loggedProcedure = publicProcedure.use(async (opts) => {
  const start = Date.now();

  const result = await opts.next();

  const durationMs = Date.now() - start;
  const meta = { path: opts.path, type: opts.type, durationMs };

  result.ok
    ? console.log('OK request timing:', meta)
    : console.error('Non-OK request timing', meta);

  return result;
});
```

```ts no-check
import { loggedProcedure, router } from './trpc';

export const appRouter = router({
  foo: loggedProcedure.query(() => 'bar'),
  abc: loggedProcedure.query(() => 'def'),
});
```

## Context Extension

"Context Extension" enables middlewares to dynamically add and override keys on a base procedure's context in a typesafe manner.

Below we have an example of a middleware that changes properties of a context, the changes are then available to all chained consumers, such as other middlewares and procedures:

```ts no-check
type Context = {
  // user is nullable
  user?: {
    id: string;
  };
};

const protectedProcedure = publicProcedure.use(async function isAuthed(opts) {
  const { ctx } = opts;
  // `ctx.user` is nullable
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  return opts.next({
    ctx: {
      // ✅ user value is known to be non-null now
      user: ctx.user,
    },
  });
});

protectedProcedure.query((opts) => {
  const { ctx } = opts;
  return ctx.user;
});
```

The `ctx` you pass to `opts.next()` is merged into the existing context, so you only need to pass the keys you add or override.

## Middleware order and input

Middlewares run in the order they're chained, and the parser you pass to `.input()` runs at its position in that chain. A middleware chained before `.input()` gets `opts.input` as `undefined`; it can read the unvalidated input with `opts.getRawInput()` instead.

```ts no-check
const auditedProcedure = publicProcedure.use(async (opts) => {
  const rawInput = await opts.getRawInput();
  console.log(`${opts.type} ${opts.path}`, rawInput);
  return opts.next();
});

auditedProcedure
  .input(z.object({ id: z.string() }))
  .use((opts) => {
    // after `.input()`, `opts.input` is parsed and typed
    console.log(opts.input.id);
    return opts.next();
  })
  .query((opts) => opts.input.id);
```

Besides `ctx`, `input`, `getRawInput` and `next`, a middleware receives the procedure's `path`, `type` (`'query'`, `'mutation'` or `'subscription'`), [`meta`](https://trpc.io/docs/server/metadata), and the request's `signal`.

## Using `.concat()` to create reusable middlewares and plugins

> [!TIP]
>
> - Creating middlewares using `t.middleware` has the limitation that the `Context` type is tied to the `Context` type of the tRPC instance.
> - Creating middlewares with `experimental_standaloneMiddleware()` has the limitation that you cannot define input parsers and similar tied to your module.


tRPC has an API called `.concat()` which allows you to independently define a partial procedure that can be used with any tRPC instance that matches the context and metadata of the plugin.

This helper primarily targets creating plugins and libraries with tRPC.


```ts no-check
// ------------------------------------------------
// 🧩🧩🧩 a library creating a reusable plugin 🧩🧩🧩

import { initTRPC, TRPCError } from '@trpc/server';

export function createMyPlugin() {
  // When creating a plugin for tRPC, you use the same API as creating any other tRPC-app
  // this is the plugin's root `t`-object
  const t = initTRPC
    .context<{
      // the procedure using the plugin will need to extend this context
    }>()
    .meta<{
      // the base `initTRPC`-object of the application using this needs to extend this meta
    }>()
    .create();

  return {
    // you can also add `.input()` if you want your plugin to do input validation
    pluginProc: t.procedure.use((opts) => {
      return opts.next({
        ctx: {
          fromPlugin: 'hello from myPlugin' as const,
        },
      });
    }),
  };
}
// ------------------------------------
// 🚀🚀🚀 the app using the plugin 🚀🚀🚀
import { createMyPlugin } from './myPlugin';
import { initTRPC, TRPCError } from '@trpc/server';


// the app's root `t`-object
const t = initTRPC
  .context<{
    // ...
  }>()
  .create();


export const publicProcedure = t.procedure;
export const router = t.router;

// initialize the plugin (a real-world example would likely take options here)
const plugin = createMyPlugin();

// create a base procedure using the plugin
const procedureWithPlugin = publicProcedure
  .concat(
    plugin.pluginProc,
  )
  .use(opts => {
    const { ctx } = opts;
    return opts.next()
  })


export const appRouter = router({
  hello: procedureWithPlugin.query(opts => {
    return opts.ctx.fromPlugin;
  })
})
```

## Extending middlewares

> [!NOTE] Info
> We have prefixed this as `unstable_` as it's a new API, but you're safe to use it! [Read more](https://trpc.io/docs/faq#unstable).


We have a powerful feature called `.pipe()` which allows you to extend middlewares in a typesafe manner.

Below we have an example of a middleware that extends a base middleware(foo). Like the context extension example above, piping middlewares will change properties of the context, and procedures will receive the new context value.

```ts no-check
const fooMiddleware = t.middleware((opts) => {
  return opts.next({
    ctx: {
      foo: 'foo' as const,
    },
  });
});

const barMiddleware = fooMiddleware.unstable_pipe((opts) => {
  const { ctx } = opts;
  ctx.foo;
  return opts.next({
    ctx: {
      bar: 'bar' as const,
    },
  });
});

const barProcedure = publicProcedure.use(barMiddleware);
barProcedure.query((opts) => {
  const { ctx } = opts;
  return ctx.bar;
});
```

Beware that the order in which you pipe your middlewares matter and that the context must overlap. An example of a forbidden pipe is shown below. Here, the `fooMiddleware` overrides the `ctx.a` while `barMiddleware` still expects the root context from the initialization in `initTRPC` - so piping `fooMiddleware` with `barMiddleware` would not work, while piping `barMiddleware` with `fooMiddleware` does work.

```ts no-check
import { initTRPC } from '@trpc/server';

const t = initTRPC
  .context<{
    a: {
      b: 'a';
    };
  }>()
  .create();

const fooMiddleware = t.middleware((opts) => {
  const { ctx } = opts;
  ctx.a; // 👈 fooMiddleware expects `ctx.a` to be an object
  return opts.next({
    ctx: {
      a: 'a' as const, // 👈 `ctx.a` is no longer an object
    },
  });
});

const barMiddleware = t.middleware((opts) => {
  const { ctx } = opts;
  ctx.a; // 👈 barMiddleware expects `ctx.a` to be an object
  return opts.next({
    ctx: {
      foo: 'foo' as const,
    },
  });
});

// ❌ `ctx.a` does not overlap from `fooMiddleware` to `barMiddleware`
fooMiddleware.unstable_pipe(barMiddleware);

// ✅ `ctx.a` overlaps from `barMiddleware` and `fooMiddleware`
barMiddleware.unstable_pipe(fooMiddleware);
```

## Common mistakes

- **Not returning `opts.next()`.** Every middleware must call `opts.next()` and return its result. Otherwise TypeScript reports an error, and at runtime the call fails with an `INTERNAL_SERVER_ERROR` ("No result from middlewares - did you forget to `return next()`?").
- **Wrapping `opts.next()` in `try`/`catch`.** `opts.next()` doesn't throw when a later middleware or the procedure fails: it resolves to a result with `ok: false` and the `TRPCError`. Check `result.ok`, like the [logging example](https://trpc.io/docs/server/middlewares#logging) does:

```ts no-check
const reportedProcedure = publicProcedure.use(async (opts) => {
  const result = await opts.next();
  if (!result.ok) {
    console.error(`${opts.path} failed`, result.error);
  }
  return result;
});
```

## Experimental: standalone middlewares

> [!WARNING] Deprecated
> This has been deprecated in favor of [`.concat()`](https://trpc.io/docs/server/middlewares#concat). Use `.concat()` for new code.


tRPC has an experimental API called `experimental_standaloneMiddleware` which allows you to independently define a middleware that can be used with any tRPC instance. Creating middlewares using `t.middleware` has the limitation that
the `Context` type is tied to the `Context` type of the tRPC instance. This means that you cannot use the same middleware with multiple tRPC instances that have different `Context` types.

Using `experimental_standaloneMiddleware` you can create a middleware that explicitly defines its requirements, i.e. the Context, Input and Meta types:

```ts no-check
import {
  experimental_standaloneMiddleware,
  initTRPC,
  TRPCError,
} from '@trpc/server';
import * as z from 'zod';

const projectAccessMiddleware = experimental_standaloneMiddleware<{
  ctx: { allowedProjects: string[] }; // defaults to 'object' if not defined
  input: { projectId: string }; // defaults to 'unknown' if not defined
  // 'meta', not defined here, defaults to 'object | undefined'
}>().create((opts) => {
  if (!opts.ctx.allowedProjects.includes(opts.input.projectId)) {
    throw new TRPCError({
      code: 'FORBIDDEN',
      message: 'Not allowed',
    });
  }

  return opts.next();
});

const t1 = initTRPC
  .context<{
    allowedProjects: string[];
  }>()
  .create();

// ✅ `ctx.allowedProjects` satisfies "string[]" and `input.projectId` satisfies "string"
const accessControlledProcedure = t1.procedure
  .input(z.object({ projectId: z.string() }))
  .use(projectAccessMiddleware);

// ❌ `ctx.allowedProjects` satisfies "string[]" but `input.projectId` does not satisfy "string"
const accessControlledProcedure2 = t1.procedure
  .input(z.object({ projectId: z.number() }))
  .use(projectAccessMiddleware);

// ❌ `ctx.allowedProjects` does not satisfy "string[]" even though `input.projectId` satisfies "string"
const t2 = initTRPC
  .context<{
    allowedProjects: number[];
  }>()
  .create();

const accessControlledProcedure3 = t2.procedure
  .input(z.object({ projectId: z.string() }))
  .use(projectAccessMiddleware);
```

Here is an example with multiple standalone middlewares:

```ts no-check
import { experimental_standaloneMiddleware, initTRPC } from '@trpc/server';
import * as z from 'zod';

const t = initTRPC.create();
const schemaA = z.object({ valueA: z.string() });
const schemaB = z.object({ valueB: z.string() });

const valueAUppercaserMiddleware = experimental_standaloneMiddleware<{
  input: z.infer<typeof schemaA>;
}>().create((opts) => {
  return opts.next({
    ctx: { valueAUppercase: opts.input.valueA.toUpperCase() },
  });
});

const valueBUppercaserMiddleware = experimental_standaloneMiddleware<{
  input: z.infer<typeof schemaB>;
}>().create((opts) => {
  return opts.next({
    ctx: { valueBUppercase: opts.input.valueB.toUpperCase() },
  });
});

const combinedInputThatSatisfiesBothMiddlewares = z.object({
  valueA: z.string(),
  valueB: z.string(),
  extraProp: z.string(),
});

t.procedure
  .input(combinedInputThatSatisfiesBothMiddlewares)
  .use(valueAUppercaserMiddleware)
  .use(valueBUppercaserMiddleware)
  .query(
    ({
      input: { valueA, valueB, extraProp },
      ctx: { valueAUppercase, valueBUppercase },
    }) =>
      `valueA: ${valueA}, valueB: ${valueB}, extraProp: ${extraProp}, valueAUppercase: ${valueAUppercase}, valueBUppercase: ${valueBUppercase}`,
  );
```

## Related skills

- `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`
