1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJSAdvent Calendar 2024

Day 15

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

Posted at

はじめに

こんにちは、梅雨です。

この記事では、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 用のモジュールをインポートします。

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

$ npx nest g resource users

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

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

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 リクエストに対応する

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

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;
  }
}

モジュールは以下のようにします。ポイントは、UsersRouterproviders に含めることです。これによって、ルータが AppModule に認識され、ルーティングの解決が行われます。

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 が自動で更新され、以下のようになります。

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

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

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 を叩くことができます。

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 などの紹介記事も書ければと思います。

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?