nestjs-trpcが通常のNestJSと違うところ
はじめに
NestJSは、TypeScript製のサーバーサイドフレームワークです。モダンなアプリケーション開発を効率化するために設計されており、スケーラブルで保守性の高いバックエンドシステムを構築する際に非常に有用です。
またtRPC(TypeScript Remote Procedure Call)は、TypeScriptを活用した型安全なリモートプロシージャコール(RPC)ライブラリです。主にフロントエンドとバックエンド間での通信をシンプルかつ効率的に行うために設計されており、型の整合性を保ちながら迅速な開発を可能にします。tRPCは特にフルスタックTypeScriptアプリケーションにのみ適用できます。
これらのNestJSとtRPCを組み合わせたnestjs-trpcというライブラリが2024年にStableになりました。
現在開発しているT3-Turboを中心としたアーキテクチャの中で、nestjs-trpcを重宝しています。T3-Turboについては以下の記事を参照ください。
本記事では、nestjs-trpcが通常のNestJSとどう違うのかを中心に紹介します。
Controller → Router
通常のNestJSはRequest/Responseの処理にControllerを使いますが、nestjs-trpcではRouterを使用します。
通常のNestJS(RESTの例)
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
constructor(@Inject(UserService) private readonly userService: UserService) {}
@Get(':userId')
getUserById(@Param('userId') userId: string): Promise<User> {
const user = await this.userService.getUser(userId);
return user;
}
}
通常のNestJSは、このように@Controller('users')や @Get, @Post を使用してルーティングを定義します。
nestjs-trpcの例
import { Inject } from '@nestjs/common';
import { Router, Query, Input } from 'nestjs-trpc';
import { UserService } from './user.service';
import { z } from 'zod';
@Router({ alias: 'users' })
export class UserRouter {
constructor(@Inject(UserService) private readonly userService: UserService) {}
@Query({
input: z.object({ userId: z.string() }),
output: userSchema,
})
async getUserById(@Input('userId') userId: string): Promise<User> {
const user = await this.userService.getUser(userId);
return user;
}
}
nestjs-trpc では、“関数” (procedure) の呼び出し というイメージでエンドポイントを定義します。query() は GET 的な読み取り操作、mutation() は POST/PUT/DELETE 的な書き込み操作という位置づけです。
URLやHTTPメソッドを直接指定しなくても、フロントエンドでは メソッドを呼び出す → データが返る というRPCの感覚で利用できるようになります。
Moduleを使ってNestJSアプリに組み込む
通常の NestJS では、AppModuleにControllerやService をインポートして組み立てます。
一方、nestjs-trpcではNestJSのModule機能を活かしつつ、Routerはprovidersに登録します。
通常のNestJS(RESTの例)
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule {}
nestjs-trpcの例
import { Module } from '@nestjs/common';
import { UserRouter } from './user.router';
import { UserService } from './user.service';
@Module({
controllers: [],
providers: [UserRouter, UserService],
})
export class UserModule {}
import { Module } from '@nestjs/common';
import { TRPCModule } from 'nestjs-trpc';
@Module({
imports: [
TRPCModule.forRoot({
autoSchemaFile: './src/@generated',
}),
],
})
export class AppModule {}
- nestjs-trpcの機能により@generated配下が自動生成される
- TrpcModule.forRouter() で自動生成された複数のプロシージャを登録
これにより、NestJS のモジュール構造を維持しつつ、Controllerを書かずにtRPCのエンドポイントを立てられるのが大きな違いです。
型の共有とZodでのバリデーション
通常のNestJSではDTOクラスやclass-validatorを使い、必要に応じてSwagger連携やOpenAPIスキーマを活用するアプローチが一般的です。
nestjs-trpcでは、tRPCがデフォルトでスキーマバリデーションライブラリのZodと相性が良く、サーバー・クライアント間で同じ型定義を共有しやすくなります。
通常のNestJS(RESTの例)
import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
export class UserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
nestjs-trpcの例
import { z } from 'zod';
export const userSchema = z.object({
name: z.string(),
email: z.string().email(),
password: z.string(),
});
export type User = z.infer<typeof userSchema>;
サーバー側でz.object({ name: z.string() })を定義すると、クライアント側でも同じ型 { name: string } として扱われます。
REST+DTOでも型の同期は可能ですが、tRPCはそれを自動化しやすく、さらにRPCスタイルで関数呼び出しのように扱えるというメリットがあります。
TRPCMiddleware
このアダプタを使用すると、プロシージャミドルウェアを作成できます。これらのミドルウェアは、ルーター内のすべてのプロシージャに対してグローバルに追加することも、@UseMiddlewares()デコレータを使用して特定のプロシージャに個別に追加することもできます。
import { MiddlewareOptions, MiddlewareResponse, TRPCMiddleware } from 'nestjs-trpc';
import { Inject, Injectable } from '@nestjs/common';
import { UserService } from './user.service';
import { TRPCError } from '@trpc/server';
@Injectable()
export class ProtectedMiddleware implements TRPCMiddleware {
constructor(@Inject(UserService) private readonly userService: UserService) {}
async use(opts: MiddlewareOptions<object>): Promise<MiddlewareResponse> {
const start = Date.now();
const result = await opts.next({
ctx: {
ben: 1,
},
});
const durationMs = Date.now() - start;
const meta = { path: opts.path, type: opts.type, durationMs };
result.ok
? console.log('OK request timing:', meta)
: console.error('Non-OK request timing', meta);
return result;
}
}
import { ProtectedMiddleware } from './protected.middleware';
@Router({ alias: 'users' })
export class UserRouter {
constructor(@Inject(UserService) private readonly userService: UserService) {}
@UseMiddlewares(ProtectedMiddleware)
async getUserById(...)
}
このように、nestjs-trpcには独自のmiddleware機構があります。
nestjs-trpcを導入するメリット
-
クライアントとサーバーが同じ型定義を共有
tRPC 最大の強みである 「型を共有しながらAPIを呼び出せる」恩恵を、NestJSというエンタープライズ向けフレームワークの世界で活かせます。
APIの変更に対して型チェックが効くため、フロント側・バックエンド側の整合性崩れが起きにくくなります。 -
NestJSのエコシステムを活かしやすい
DI(Dependency Injection)でビジネスロジックやRepositoryを分割管理します。
Guard, Interceptor, Middleware, Exception Filterなど、NestJSの強力な仕組みをprocedureに適用できます。
Module単位で機能をまとめられるため、大規模開発にも向いています。 -
Controller / HTTPデコレーターを省略できる
RESTスタイルのNestJSではコントローラーの作成やルート設定が必須ですが、nestjs-trpcであれば“procedure”によるRPCスタイルでシンプルにエンドポイントを定義できます。
API 設計を“URLやメソッド単位”ではなく“関数呼び出し単位”で考えたい場合、チームの開発効率が向上します。
おまけ:trpc-panel
便利だったので共有します。tRPCエンドポイントのテストUIとドキュメントを簡単に構築します。Swagger UIのtRPC版のようなイメージです。
nestjs-trpcへの導入は以下参照。
まとめ
筆者の完全なる主観ですが、"nestjs-trpc"で1つのフレームワークという印象を受けました。NestJSがさらにモダナイズした使い心地になっており、最先端でかつ大規模開発に対応できるバックエンドフレームワークがようやく出てきて嬉しい限りです。読者の皆さんも、ぜひ新規開発の際は導入を検討してみてください。

フィシルコムのテックブログです。マーケティングSaaSを開発しています。 マイクロサービス・AWS・NextJS・Golang・GraphQLに関する発信が多めです。 カジュアル面談はこちら(ficilcom.notion.site/bbceed45c3e8471691ee4076250cd4b1)から
Discussion