Docs
Middlewares

Middlewares

With this adapter you can create procedure middlewares, These middlewares can be added to a router class, either globally for all procedures in that router or individually to specific procedures using the @UseMiddlewares() decorator.

The @UseMiddlewares() decorator accepts multiple middlewares, executing them in the provided order. For instance:

@UseMiddlewares(LoggingMiddleware, AuthMiddleware)

In this example, LoggingMiddleware runs first, followed by AuthMiddleware.
If both the router and the procedure have middlewares defined via @UseMiddlewares(), the router-level middlewares will execute first, followed by the procedure-level middlewares, provided there are no duplicates.
This middleware approach in tRPC is similar to the Guards and Middlewares concept found in NestJS.

💡

If you are not sure about the basic concepts of NestJS or tRPC middlewares, you can dive into those concepts in their official documentation.

Creating a Middleware

You can implement a middleware in a class with an @Injectable() decorator. The class should implement the TRPCMiddleware interface. Let's start by implementing a simple logger middleware.

logger.middleware.ts
import { TRPCMiddleware, TRPCMiddlewareOptions } from 'nestjs-trpc';
import { Inject, Injectable, ConsoleLogger } from '@nestjs/common';
import type { Context } from "nestjs-trpc/types";
 
@Injectable()
export class LoggedMiddleware implements TRPCMiddleware<Context> {
 
  constructror(
    @Inject(ConsoleLogger) private readonly consoleLogger: ConsoleLogger
  ) {}
 
  use(opts: TRPCMiddlewareOptions) {
    const start = Date.now();
    const { next, path, type } = opts;
    const result = await next();
 
    const durationMs = Date.now() - start;
    const meta = { path, type, durationMs }
 
    result.ok
      ? this.consoleLogger.log('OK request timing:', meta)
      : this.consoleLogger.error('Non-OK request timing', meta);
 
    return result;
  }
}

Middleware registration

Similar to NestJS providers, we need to register the middleware with Nest so that it can perform the injection and type generation. We do this by editing our module file and adding the middleware to the providers array of the @Module() decorator.

Applying middleware

To use the middleware in your procedure instead of the default publicProcedure, pass the class to the @UseMiddlewares() method decorator.

dogs.router.ts
import { DatabaseService } from "./database.service.ts";
import { LoggedMiddleware } from "./logged.middleware.ts";
import { AuthMiddleware } from "./auth.middleware.ts";
import { Router, Query, UseMiddlewares } from 'nestjs-trpc';
import { Inject } from '@nestjs/common';
import { z } from 'zod';
 
const dogsSchema = z.object({
  name: z.string(),
  breed: z.enum(["Labrador", "Corgi", "Beagle", "Golden Retriver"])
});
 
@Router()
export class DogsRouter {
  constructor(@Inject(DatabaseService) private databaseService: DatabaseService){}
 
  @UseMiddlewares(LoggedMiddleware, AuthMiddleware)
  @Query({ output: z.array(dogSchema) })
  async findAll(): string {
    const dogs = await this.databaseService.dogs.findMany();
    return dogs;
  }
}

Global Middlewares

If you need to apply a middleware to every procedure in your application (e.g., logging, error reporting, analytics), you can use the globalMiddlewares option in TRPCModule.forRoot() instead of decorating each router individually.

app.module.ts
import { Module } from '@nestjs/common';
import { TRPCModule } from 'nestjs-trpc';
import { LoggedMiddleware } from './logged.middleware';
import { ErrorReportingMiddleware } from './error-reporting.middleware';
 
@Module({
  imports: [
    TRPCModule.forRoot({
      globalMiddlewares: [LoggedMiddleware, ErrorReportingMiddleware],
    }),
  ],
  providers: [LoggedMiddleware, ErrorReportingMiddleware],
})
export class AppModule {}
💡

Execution order: Global middlewares → Router middlewares (@UseMiddlewares() on class) → Procedure middlewares (@UseMiddlewares() on method) → Handler.

Global middlewares have full dependency injection support — they work exactly like middlewares applied with @UseMiddlewares(), including context modification and generated types.

Modified Context

With middlewares you can change and inject certain parameters in the context to be available to the procedure, this can be done by using the next() method available from the opts parameter.

Typed Return Context

MiddlewareOptions accepts a second generic, TReturnContext, that types the ctx object you pass to next(). This gives you compile-time safety on the context shape your middleware provides to downstream procedures:

MiddlewareOptions<TContext, TReturnContext, TMeta>
GenericDefaultDescription
TContextobjectThe incoming context type available via opts.ctx
TReturnContextRecord<string, unknown>The context shape passed to opts.next({ ctx })
TMetaunknownThe procedure metadata type available via opts.meta
Generated Context

When you modify the base context with a middleware, a new type will be generated with the middleware name and a Context suffix, for example if you have an AuthMiddleware, the generated type would be named AuthMiddlewareContext.

Then you can import those types and use throughout your procedures from nestjs-trpc/types, for example:

user.router.ts
import type { AuthMiddlewareContext } from 'nestjs-trpc/types';
import { UserService } from "./user.service.ts";
import { AuthMiddleware } from "./auth.middleware.ts";
import { Router, Query, UseMiddlewares } from 'nestjs-trpc';
import { Inject } from '@nestjs/common';
import { z } from 'zod';
 
const userSchema = z.object({
  name: z.string(),
  email: z.string(),
  avatar: z.string()
});
 
@Router()
export class UserRouter {
  constructor(@Inject(UserService) private userService: UserService){}
 
  @UseMiddlewares(AuthMiddleware)
  @Query({ output: userSchema })
  async getUserProfile(@Context() ctx: AuthMiddlewareContext): string {
    const { userId } = ctx.auth;
    return await this.userService.getUserById(userId);
  }
}
💡

The TReturnContext generic defaults to Record<string, unknown>, so existing middlewares that don't use it continue to work without changes.

Procedure Metadata

Procedures can define a meta object that middlewares can read at runtime. This is the tRPC-idiomatic way to implement authorization and other cross-cutting concerns — similar to how NestJS guards use @SetMetadata() and Reflector.

Defining Meta

Pass a meta object to the @Query() or @Mutation() decorator:

admin.router.ts
import { Router, Query, Mutation, UseMiddlewares, Input } from 'nestjs-trpc';
import { z } from 'zod';
 
@Router({ alias: 'admin' })
@UseMiddlewares(RolesMiddleware)
export class AdminRouter {
  @Query({
    input: z.object({ userId: z.string() }),
    meta: { roles: ['admin'] },
  })
  async getUser(@Input('userId') userId: string) {
    return this.userService.getUser(userId);
  }
 
  @Mutation({
    input: z.object({ userId: z.string() }),
    meta: { roles: ['admin', 'moderator'] },
  })
  async deleteUser(@Input('userId') userId: string) {
    return this.userService.deleteUser(userId);
  }
}
Reading Meta in Middleware

The TRPCMiddleware interface accepts a generic type parameter for meta, and MiddlewareOptions accepts generics for context, return context, and meta — giving you full type safety without assertions:

roles.middleware.ts
import {
  MiddlewareOptions,
  MiddlewareResponse,
  TRPCMiddleware,
} from 'nestjs-trpc';
import { Injectable } from '@nestjs/common';
import { TRPCError } from '@trpc/server';
import type { AuthMiddlewareContext } from 'nestjs-trpc/types';
 
interface RolesMeta {
  roles: string[];
}
 
@Injectable()
export class RolesMiddleware implements TRPCMiddleware<RolesMeta> {
  async use(opts: MiddlewareOptions<AuthMiddlewareContext, Record<string, unknown>, RolesMeta>): Promise<MiddlewareResponse> {
    const { meta, ctx, next } = opts;
 
    if (!meta.roles.includes(ctx.auth.userId)) {
      throw new TRPCError({
        message: 'Insufficient permissions.',
        code: 'FORBIDDEN',
      });
    }
 
    return next();
  }
}
💡

The TRPCMiddleware<TMeta> generic defaults to unknown, so existing middlewares that don't use meta continue to work without changes.

Dependency injection

UseMiddlewares fully supports Dependency Injection. Just as with NestJS middlewares and guards, they are able to inject dependencies that are available within the same module. As usual, this is done through the constructor.