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, Middlewares, 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,
    })
    @Middlewares(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 only used for type inference, thats why you don't see any logic within .query() method and why we have a 'PLACEHOLDER_DO_NOT_REMOVE' as any as the logic just to keep typescript happy. Your router business logic is then executed during run-time with vanilla trpc and the selected driver (express or fastify) based on your configurations.

💡

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.