Migration from REST or GraphQL
This guide helps you migrate existing NestJS APIs from REST controllers or GraphQL resolvers to nest-trpc-native routers.
If you also want a side-by-side comparison with WebSocket and gRPC architectural patterns, see Transport Pattern Parallels.
Migration Strategy
Use an incremental rollout instead of a big-bang rewrite:
- Keep existing REST/GraphQL endpoints running.
- Add
TrpcModuleand create one new router for a non-critical feature. - Move shared business logic into services (if not already).
- Migrate endpoints/resolvers one feature at a time.
- Keep guards, pipes, interceptors, and filters as-is.
- Switch clients to typed tRPC procedures gradually.
Concept Mapping
| REST (Nest) | GraphQL (Nest) | tRPC (nest-trpc-native) |
|---|---|---|
@Controller('users') | @Resolver() | @Router('users') |
@Get(), @Post() | @Query(), @Mutation() | @Query(), @Mutation() |
@Param(), @Body(), @Query() | @Args() | @Input() |
@Req() / custom context | @Context() | @TrpcContext() |
DTO + ValidationPipe | Input types + pipes | DTO + ValidationPipe or Zod input schema |
| Guards/interceptors/filters | Guards/interceptors/filters | Same Nest enhancers |
REST Controller -> tRPC Router
Before (REST)
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
byId(@Param('id') id: string) {
return this.usersService.byId(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
After (tRPC)
import { Input, Mutation, Query, Router } from 'nest-trpc-native';
import { UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
@Router('users')
@UseGuards(AuthGuard)
export class UsersRouter {
constructor(private readonly usersService: UsersService) {}
@Query()
byId(@Input('id') id: string) {
return this.usersService.byId(id);
}
@Mutation()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Input() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
Main change: transport decorators (@Get, @Post, @Body, @Param) become procedure decorators (@Query, @Mutation) and @Input().
GraphQL Resolver -> tRPC Router
Before (GraphQL)
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
@Resolver()
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => User)
user(@Args('id') id: string) {
return this.usersService.byId(id);
}
@Mutation(() => User)
createUser(@Args('input') input: CreateUserInput) {
return this.usersService.create(input);
}
}
After (tRPC)
import { Input, Mutation, Query, Router } from 'nest-trpc-native';
@Router('users')
export class UsersRouter {
constructor(private readonly usersService: UsersService) {}
@Query()
user(@Input('id') id: string) {
return this.usersService.byId(id);
}
@Mutation()
createUser(@Input() input: CreateUserInput) {
return this.usersService.create(input);
}
}
Main change: GraphQL schema decorators and return type metadata are replaced by procedure methods with TypeScript-first inference.
Middleware, Guards, and Interceptors
If you already use Nest enhancers, most code is reused unchanged:
@UseGuards(...)on class/method still works.@UseInterceptors(...)still wraps execution.@UseFilters(...)still controls exception remapping.@UsePipes(...)still applies validation/transforms.
Your migration should focus on replacing transport decorators, not rewriting business logic or enhancer logic.
Validation Choices (Zod Optional)
Both approaches are supported:
DTO + class-validator
@Mutation()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Input() dto: CreateUserDto) {
return this.usersService.create(dto);
}
Zod schema
const CreateUserSchema = z.object({ name: z.string().min(1) });
@Mutation({ input: CreateUserSchema })
create(@Input() input: { name: string }) {
return this.usersService.create(input);
}
Choose one style per module for consistency. If your codebase already uses DTOs, keep DTOs. Zod is optional.
Request/Response Handling Differences
- In tRPC, you generally do not write to
resmanually. - Return values become typed procedure outputs.
- Request metadata should come from
createContextand@TrpcContext(). - For cross-cutting concerns (auth, logging, errors), keep using Nest enhancers.
Recommended Migration Order
- Start with read-only endpoints (
GET/ GraphQL queries). - Migrate one mutation flow with validation.
- Migrate endpoints that rely on guards/interceptors.
- Add typed client usage (
AppRouter) in one consumer. - Remove legacy endpoints only after client traffic is switched.