Docs
Routers

Routers

Routes are responsible for handling incoming requests and returning responses to the client. Routers works exactly like the native NestJS controllers, but with some tRPC syntax.


đź’ˇ

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

Writing Routers

Let's compare the most basic functionality of tRPC and see how we implement it in NestJS with our adapter, in the following example we'll create a simple router with a single route that will get us a list of dogs from our imaginary database.

dogs.router.ts
import { DatabaseService } from "./database.service.ts";
import { Router, Query } 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){}
 
  @Query({ output: z.array(dogSchema) })
  async findAll(): string {
    const dogs = await this.databaseService.dogs.findMany();
    return dogs;
  }
}

In that example we used the @Router() decorator, which is required to define a basic router.
The @Query() decorator before the findAll() method tells NestJS tRPC to create a tRPC query for the specified router and output schema.

đź’ˇ

The @Router() decorator can also take an alias: string parameter that will change it's name when applied to the trpc router. @Router(alias?: string).

Router registration

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

Method Decorators

Procedures

While we discussed how to perform data fetching with our adapter, any complete data platform needs a way to modify server-side data as well. This can be done by using the right decorator, here is a list of the available procedure decorators:


DecoratorOutput
@Query({ input?: ZodSchema, output?: ZodSchema })
publicProcedure.query()
@Mutation({ input?: ZodSchema, output?: ZodSchema })
publicProcedure.mutation()
@Subscription({ input?: ZodSchema, output?: ZodSchema })
publicProcedure.subscription()

tRPC procedures may define validation logic for their input and/or output, and validators are also used to infer the types of inputs and outputs in the client-side. We can pass those schemas directly to the decorator as shown above.
For me information on how tRPC procedure validation works, check their official documentation page.

Middlewares

By default every route will be assigned the publicProcedure base, but it is possible to use custom middlewares instead our router via the @UseMiddlewares() decorator.

DecoratorOutput
@UseMiddlewares(middleware: TRPCMiddleware)
middleware.query()

You can read more about the implementation in our Middlewares Guide.

Parameter Decorators

In order to grab the request parameters when defining a route method we can use dedicated decorators, such as @Ctx() or @Input(), which are available out of the box. Below is a list of the provided decorators and the plain platform-specific objects they represent.


@Options()
opts
@Context()
opts.ctx
@Path()
opts.path
@Type()
opts.type
@RawInput()
opts.rawInput
@Input(key?: string)
opts.input / opts.input[key]

Putting it all together

Below is an example of a UserRouter completed with most of the features and decorators mentioned above.

user.router.ts
import { Inject } from '@nestjs/common';
import { Router, Query, UseMiddlewares, Input } from 'nestjs-trpc';
import { UserService } from './user.service';
import { ProtectedMiddleware } from './protected.middleware';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
 
const userSchema = z.object({
  name: z.string(),
  email: z.string(),
  password: z.string(),
});
 
type User = z.infer<typeof userSchema>;
 
@Router({ alias: 'users' })
export class UserRouter {
  constructor(@Inject(UserService) private readonly userService: UserService) {}
 
  @Query({
    input: z.object({ userId: z.string() }),
    output: userSchema,
  })
  @UseMiddlewares(ProtectedMiddleware)
  async getUserById(@Input('userId') userId: string): Promise<User> {
    const user = await this.userService.getUser(userId);
 
    if (user == null) {
      throw new TRPCError({
        message: 'Could not find user.',
        code: 'NOT_FOUND',
      });
    }
 
    return user;
  }
}

Dependency injection

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