はじめに
こんにちは、梅雨です。
この記事では、Node.js のフレームワークである NestJS で tRPC を行うことのできるライブラリである nestjs-trpc
を用いたバックエンド開発の方法を紹介していこうと思います。
環境構築
$ mkdir nestjs-trpc-demo
$ cd nestjs-trpc-demo
$ npx nest new server
$ npx create-next-app@latest client
プロジェクト構造は以下のようになっています。
nestjs-trpc-demo
├── client
│ ├── README.md
│ ├── eslint.config.mjs
│ ├── next-env.d.ts
│ ├── next.config.ts
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ ├── src
│ ├── tailwind.config.ts
│ └── tsconfig.json
└── server
├── README.md
├── nest-cli.json
├── node_modules
├── package-lock.json
├── package.json
├── src
├── test
├── tsconfig.build.json
└── tsconfig.json
nestjs-trpc を導入
サーバ側のアプリケーションで nestjs-trpc をインストールします。
$ npm i nestjs-trpc
続いて、app.module.ts
で tRPC 用のモジュールをインポートします。
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
+ import { TRPCModule } from "nestjs-trpc";
@Module({
- imports: [],
+ imports: [
+ TRPCModule.forRoot({
+ autoSchemaFile: "./src/@generated",
+ }),
+ ],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
準備はこれだけです。この状態でサーバを起動すると、src/@generated/server.ts
が自動的に生成されます。
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const publicProcedure = t.procedure;
const appRouter = t.router({});
export type AppRouter = typeof appRouter;
ルーティングの作成
ここからは先ほど生成されたファイルを編集し、実際にルーティングを作成していきましょう。
まずは nest g resource
コマンドでリソースを作成します。
$ npx nest g resource users
続いて、server/src/users/users.controller.ts
を server/src/users/users.router.ts
を変更します。
nestjs-trpc はコントローラの代わりにルータファイルを使用します。
import { UsersService } from "./users.service";
import { Router, Query } from "nestjs-trpc";
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
name: z.string(),
});
type User = z.infer<typeof userSchema>;
@Router({ alias: "users" })
export class UsersRouter {
constructor(private usersService: UsersService) {}
@Query({ output: z.array(userSchema) })
async findAll(): Promise<User[]> {
const users = await this.usersService.findAll();
return users;
}
}
@Router()
デコレータ
ルータを定義する
alias: string
で後述のルータ名を指定できる
@Query()
デコレータ
クエリを定義する
GET リクエストに対応する
サービスは以下のようにモックのユーザデータを返却するよう記述しました。
import { Injectable } from "@nestjs/common";
@Injectable()
export class UsersService {
async findAll() {
const users = [
{ id: "1", name: "Meiyu" },
{ id: "2", name: "tsuyuni" },
];
return users;
}
}
モジュールは以下のようにします。ポイントは、UsersRouter
を providers
に含めることです。これによって、ルータが AppModule
に認識され、ルーティングの解決が行われます。
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { UsersRouter } from "./users.router";
@Module({
controllers: [],
providers: [UsersService, UsersRouter],
})
export class UsersModule {}
サーバを起動すると、src/@generated/server.ts
が自動で更新され、以下のようになります。
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const publicProcedure = t.procedure;
const appRouter = t.router({
users: t.router({
findAll: publicProcedure.output(z.array(z.object({
id: z.string(),
name: z.string(),
}))).query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any)
})
});
export type AppRouter = typeof appRouter;
エンドポイントの確認
tRPC では、ルートは /trpc
以下にマップされます。
今回作成したエンドポイントは GET /trpc/users.findAll
でアクセスすることができます。このエンドポイントを用いれば、tRPC を用いずに API を叩くことも一応可能です。
@Router()
デコレータでエイリアスを設定しない場合、デフォルトのエンドポイントは GET /trpc/usersRouter.findAll
となります。
tRPC において基本的にエンドポイントの URL を意識する必要はないので、この辺はお好みでいいと思います。
$ curl -X GET http://localhost:8000/trpc/users.findAll
{"result":{"data":[{"id":"1","name":"Meiyu"},{"id":"2","name":"tsuyuni"}]}}
クライアントの作成
今回の記事ではクライアントに Next.js を採用しています。まずは tRPC クライアントを作成しましょう。
AppRouter は自動生成された server/src/@generated/server
からインポートします。
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { AppRouter } from "../../../server/src/@generated/server";
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: "http://localhost:8000/trpc",
}),
],
});
サーバーコンポーネント内では、以下のようにして API を叩くことができます。
import { trpc } from "@/utils/trpc";
const Home = async () => {
const users = await trpc.users.findAll.query();
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default Home;
おわりに
nestjs-trpc を使うことで End to End で型安全に fetch を行うことができました。
今回は query のみの紹介となりましたが、時間のあるときに mutation や subscription などの紹介記事も書ければと思います。
最後までお読みいただきありがとうございました。