Docs
Client Usage

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:

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';
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.