Client Usage
The crux with tRPC is the automatic inference of types between the backend services and the frontend app. While this is possible with a compile-time centric approach with pure typescript, NestJS heavily relies on run-time to perfrom most of it's core features.
To adapt a compile-time centric library with a run-time centric library and facilitate this simbiosis backend-frontend approach we had to practically generate the AppRouter
from scratch based on the routers you create.
AppRouter Generation
If you enable the type generation by including a autoSchemaFile
path in the module option,
the AppRouter
types usually infered from your tRPC routers will be generated by your @Router()
.
Here is and example for a router, and the generated output:
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';
import { User, userSchema } from './user.schema';
@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;
}
}
The generated AppRouter
is used purely for type inference, which is why there’s no logic inside the .query()
method. The placeholder value 'PLACEHOLDER_DO_NOT_REMOVE' as any
is used to satisfy TypeScript. Your router's actual business logic is executed at runtime using vanilla tRPC and your chosen driver (either Express or Fastify) based on your configuration.
If the input or output Zod schema couldn't be flattened when generating the AppRouter
, we attempt to import the missing dependencies using their relative paths. If that fails, you can specify the missing imports using the schemaFileImports
option in the TRPCModule
. The adapter will include these imports in the generated AppRouter
.
If you notice the generated AppRouter
takes into concideration the @Router(alias?: string)
alias, and it hoists the original ZodSchema
even if imported from a different file (which wasn't easy to make work 😅).
Using the Client
While a tRPC API can be called using normal HTTP requests like any other REST API, you will need a client to benefit from tRPC's typesafety.
A client knows the procedures that are available in your API, and their inputs and outputs. It uses this information to give you autocomplete on your queries and mutations, correctly type the returned data, and show errors if you are writing requests that don't match the shape of your backend.