【TypeScript】NestJS 用 tRPC アダプタの NestJS-tRPC を使ってE2Eで型安全にバックエンド開発

# はじめに

こんにちは、梅雨です。

この記事では、Node.js のフレームワークである NestJS で tRPC を行うことのできるライブラリである `nestjs-trpc` を用いたバックエンド開発の方法を紹介していこうと思います。

https://www.nestjs-trpc.io

# 環境構築

```bash
$ mkdir nestjs-trpc-demo
$ cd nestjs-trpc-demo
$ npx nest new server
$ npx create-next-app@latest client
```

プロジェクト構造は以下のようになっています。

```bash
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 をインストールします。

```bash
$ npm i nestjs-trpc
```

続いて、`app.module.ts` で tRPC 用のモジュールをインポートします。

```diff_typescript:server/src/app.module.ts
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` が自動的に生成されます。

```server/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` コマンドでリソースを作成します。

```bash
$ npx nest g resource users
```

続いて、`server/src/users/users.controller.ts` を `server/src/users/users.router.ts` を変更します。

nestjs-trpc はコントローラの代わりにルータファイルを使用します。

```ts:server/src/users/users.router.ts
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 リクエストに対応する

サービスは以下のようにモックのユーザデータを返却するよう記述しました。

```ts:server/src/users/users.service.ts
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` に認識され、ルーティングの解決が行われます。
 
```ts:server/src/users/users.module.ts
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` が自動で更新され、以下のようになります。

```ts:server/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 を意識する必要はないので、この辺はお好みでいいと思います。

```bash
$ 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` からインポートします。

```ts:client/src/utils/trpc.ts
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 を叩くことができます。

```tsx:client/src/app/page.tsx
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 などの紹介記事も書ければと思います。

最後までお読みいただきありがとうございました。