Skip to content
tRPC

Middlewares

On this page

You are able to add middleware(s) to a procedure with the t.procedure.use() method. The middleware(s) will wrap the invocation of the procedure and must pass through its return value.

Authorization

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

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,
    },
  });
});
import { adminProcedure, publicProcedure, router } from './trpc';

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

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

Logging

In the example below timings for queries are logged automatically.

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;
});
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:

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(({ ctx }) => ctx.user);

Extending middlewares

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.

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(({ ctx }) => 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.

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);

Experimental: standalone middlewares

tRPC has a new 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:

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:

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}`,
  );