Skip to Content
๐Ÿ‘‹ Hey there! Welcome to NestJS tRPC.
DocumentationIntroduction

NestJS tRPC

A TypeScript integration package that bridges NestJS and tRPC, enabling fully type-safe API development without sacrificing the powerful features of NestJS.

What is NestJS tRPC?

NestJS tRPC automatically generates tRPC server definition files from NestJS decorators, allowing you to:

  • Build end-to-end typesafe APIs with NestJS as your backend framework
  • Leverage NestJS dependency injection, modules, and lifecycle while getting tRPCโ€™s type safety
  • Eliminate manual schema definition through automatic generation
  • Use familiar decorator patterns for defining tRPC routers, queries, and mutations
  • Integrate with Express and Fastify frameworks seamlessly

Key Features

โœจ Automatic Schema Generation - Generate tRPC schema files from NestJS decorators (tRPC v11)
๐Ÿ”’ Type-Safe APIs - Full end-to-end type safety without manually defining schemas
๐ŸŽฏ NestJS Decorators - Familiar @Router(), @Query(), @Mutation(), @Subscription() decorators
๐Ÿ“ฅ Parameter Decorators - @Input() and @Context() for accessing validated data and request context
โšก Middleware Support - Full tRPC middleware integration
๐Ÿ“ก Real-time Subscriptions - WebSocket support for live data updates
๐Ÿš€ Multiple Drivers - Express and Fastify support
๐Ÿ” Zod Integration - Built-in validation with Zod schemas

Quick Example

Server Usage

import { Router, Query, Mutation, Subscription, Input, Context, Middleware, createEventSubscription } from '@nexica/nestjs-trpc'
import { AuthMiddleware, AdminMiddleware } from '@/middleware'
import { z } from 'zod'
 
// Input Schemas
const GetUserInputSchema = z.object({
    id: z.string().uuid(),
    includeProfile: z.boolean().default(false),
})
 
const UserCreateInputSchema = z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    age: z.number().min(18).max(120),
})
 
const DeleteUserInputSchema = z.object({
    id: z.string().uuid(),
})
 
const UserSubscriptionInputSchema = z.object({
    userId: z.string().optional(),
})
 
// Output Schemas
const UserSchema = z.object({
    id: z.string(),
    name: z.string(),
    email: z.string(),
    age: z.number(),
    createdAt: z.date(),
})
 
const DeleteUserOutputSchema = z.object({
    success: z.boolean(),
})
 
const UserEventOutputSchema = z.object({
    type: z.enum(['created', 'updated', 'deleted']),
    user: UserSchema,
    timestamp: z.date(),
})
 
@Router()
export class UserRouter {
    // ๐Ÿ” Query with validation and context
    @Query({
        input: GetUserInputSchema,
        output: UserSchema.nullable(),
    })
    async getUser(@Input() input: z.infer<typeof GetUserInputSchema>, @Context() ctx: RequestContext) {
        const user = await this.userService.findById(input.id)
 
        if (!user) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' })
        }
 
        return user
    }
 
    // โœ๏ธ Mutation with comprehensive validation
    @Mutation({
        input: UserCreateInputSchema,
        output: UserSchema,
    })
    @Middleware(AuthMiddleware) // ๐Ÿ”’ Authentication required
    async createUser(@Input() input: z.infer<typeof UserCreateInputSchema>, @Context() ctx: RequestContext) {
        // Check if email already exists
        const existing = await this.userService.findByEmail(input.email)
        if (existing) {
            throw new TRPCError({ code: 'CONFLICT', message: 'Email already exists' })
        }
 
        return await this.userService.create(input)
    }
 
    // ๐Ÿ—‘๏ธ Admin-only deletion
    @Mutation({
        input: DeleteUserInputSchema,
        output: DeleteUserOutputSchema,
    })
    @Middleware(AuthMiddleware, AdminMiddleware) // ๐Ÿ›ก๏ธ Admin access required
    async deleteUser(@Input() input: z.infer<typeof DeleteUserInputSchema>, @Context() ctx: RequestContext) {
        await this.userService.delete(input.id)
        return { success: true }
    }
 
    // ๐Ÿ“ก Real-time subscription
    @Subscription({
        input: UserSubscriptionInputSchema,
        output: UserEventOutputSchema,
    })
    @Middleware(AuthMiddleware) // ๐Ÿ”’ Authenticated users only
    async *onUserUpdated(
        @Input() input: z.infer<typeof UserSubscriptionInputSchema>,
        @Context() ctx: RequestContext
    ): AsyncIterable<z.infer<typeof UserEventOutputSchema>> {
        const eventEmitter = this.userService.getEventEmitter()
 
        // Use the subscription helper with filtering
        return createEventSubscription(eventEmitter, 'userUpdated', {
            filter: (event) => {
                // Filter events if specific user requested
                return !input.userId || event.user.id === input.userId
            },
            transform: (event) => ({
                type: event.type,
                user: event.user,
                timestamp: new Date(),
            }),
            maxQueueSize: 100,
            timeout: 30000, // 30 second timeout
        })
    }
}

Client Usage

// Frontend usage with full type safety
const user = await trpc.user.getUser.query({
    id: '550e8400-e29b-41d4-a716-446655440000',
    includeProfile: true,
}) // Type: User | null
 
const newUser = await trpc.user.createUser.mutate({
    name: 'John Doe',
    email: 'john@example.com',
    age: 25,
}) // Type: User
 
const deleteResult = await trpc.user.deleteUser.mutate({
    id: '550e8400-e29b-41d4-a716-446655440000',
}) // Type: { success: boolean }
 
// Real-time subscription with full type safety
trpc.user.onUserUpdated.subscribe(
    {
        userId: '550e8400-e29b-41d4-a716-446655440000',
    },
    {
        onData: (data) => {
            // data is fully typed: { type: 'created' | 'updated' | 'deleted', user: User, timestamp: Date }
            console.log(`User ${data.type} at ${data.timestamp}:`, data.user.name)
        },
        onError: (error) => {
            console.error('Subscription error:', error)
        },
    }
)
Last updated on: