Chapter 03

APIを作ろう (trpc vs Action Controller)

ykpythemind
ykpythemind
2022.09.28に更新

trpcの概要

trpc はタイプセーフなAPIを実装するためのライブラリです。
スキーマやそこからのコード生成などをすることなくAPIを実装でき、クライアントとサーバサイドでコードを共有できることを売りにしています。

Introduction にある以下の点を念頭に置いておくと理解しやすそうです。

  • ✅ Well-tested and production ready. / よくテストされてるよ
  • 🧙‍♂️ Full static typesafety & autocompletion on the client, for inputs, outputs and errors. / 静的に型解析できて補完きくよ
  • 🐎 Snappy DX - No code generation, run-time bloat, or build pipeline. / コード生成とかいらないし特別なビルドとかもいらないよ
  • 🍃 Light - tRPC has zero deps and a tiny client-side footprint. / 依存なくて軽い
  • 🐻 Easy to add to your existing brownfield project. / 既存のプロジェクトに組み込みやすい
  • 🔋 Batteries included - React.js/Next.js/Express.js/Fastify adapters. (But tRPC is not tied to React and there are many community adapters for other libraries) / 他のOSSとつなぎ込みやすいアダプタを提供している
  • 🥃 Subscriptions support. / websocketにも対応している
  • ⚡️ Request batching - requests made at the same time can be automatically combined into one. / リクエストを1つにまとめて送信できる
  • 👀 Quite a few examples that you can use for reference or as a starting point. / 始めやすいように例もあるよ

RailsのActionControllerの抽象化とは全く異なるので、REST APIの思想などを一旦忘れると良さそうです。

create-t3-appで生成されたファイルの概要

create-t3-app で自動生成されているファイルを軽く眺めておきます。

src/server/trpc/router/index.ts

ルーターというものを初期化しています。ここに各procedureを登録することでよしなにエンドポイントにルーティングできるといった雰囲気のようです。

export const appRouter = t.router({
  example: exampleRouter,
  auth: authRouter,
});

src/pages/api/trpc/[trpc].ts

Next.jsのAPI Routes になります。

export default createNextApiHandler({
  router: appRouter,
  createContext,
});

先程のappRouterを登録して、Next.js上で使えるようにしています。

src/server/trpc/trpc.ts

export const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape }) {
    return shape;
  },
});

trpc v10から t というオブジェクトが trpcの基本のオブジェクトになるようです。グローバルなコンフィグやミドルウェアの登録などはここに書けば良さそうです。

src/utils/trpc.ts

export const trpc = createTRPCNext<AppRouter>(...)

Next.jsのアダプタを噛ませています。主にクライアントサイドからはこの utils/trpcを参照します。(Next.jsでは一部SSRする場合はサーバサイドからも参照することになりますが)

Procedure

では、 Procedure を実装しましょう。

ProcedureはRestエンドポイントか関数とほぼ同義とみなすことができます。queryとmutationが存在するが、これは内部の実装は変わらないとのことです。[1]

すでに存在している exampleRouterにprocedureを登録してみましょう。

diff --git a/src/server/trpc/router/example.ts b/src/server/trpc/router/example.ts
index 19bd547..e66eeec 100644
--- a/src/server/trpc/router/example.ts
+++ b/src/server/trpc/router/example.ts
@@ -12,4 +12,7 @@ export const exampleRouter = t.router({
   getAll: t.procedure.query(({ ctx }) => {
     return ctx.prisma.example.findMany();
   }),
+  myProcedure: t.procedure.query(() => {
+    return { hoge: "fuga" };
+  }),
 });

myProcedure を登録しました。queryの返り値がAPIのレスポンスとなります。
このAPIはクライアントからは client.example.myProcedure.query() のように呼び出すことができます。

動作確認として実際にリクエストを送ってみます。(普段の使用では裏側のHTTPリクエストを意識することはないです)

{ "hoge": "fuga" } の構造が入っています。このレスポンスはtrpcがハンドリングしており、 これによってRequest Batchingを表現しています。

Userの取得APIの実装

Railsチュートリアルの ユーザーページのためのAPIを実装しましょう。

  • /users ... すべてのユーザーを一覧する
  • /users/1 ... id = 1のユーザーの情報を返す

これをtrpcのprocedureで表現します。

src/server/trpc/router/user.ts を作成し、userRouterを作成します。

import { t } from "../trpc";
import { z } from "zod";
import { TRPCError } from "@trpc/server";

export const userRouter = t.router({
  getUsers: t.procedure.query(async ({ ctx }) => {
    return await ctx.prisma.user.findMany();
  }),
  getOneUser: t.procedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      const user = await ctx.prisma.user.findUnique({
        where: { id: input.userId },
      });

      if (user) {
        return user;
      } else {
        throw new TRPCError({
          code: "NOT_FOUND",
        });
      }
    }),
});

getUsers の方は分かりやすいと思います。 ctxから prisma clientのインスタンスを引くことができるので、そこからfindMany() で一覧取得しています。

getOneUser では input() を指定して、入力のバリデーションを行っています。デフォルトでは zod で検証を行うようになっています。入力のバリデーションについては別チャプターで詳細に解説します。

ここでは、stringでuserIdを入力として与える必要があることを宣言しています。

user.findUniqueで与えられたuserIdからユーザーを探し、なければ TRPCErrorをthrowします。TRPCErrorをthrowしておくことで trpcのエラーハンドリングの仕組みに乗っかることができます。

最後に、忘れずにrouterに登録します。

--- a/src/server/trpc/router/index.ts
+++ b/src/server/trpc/router/index.ts
@@ -2,10 +2,12 @@
 import { t } from "../trpc";
 import { exampleRouter } from "./example";
 import { authRouter } from "./auth";
+import { userRouter } from "./user";
 
 export const appRouter = t.router({
   example: exampleRouter,
   auth: authRouter,
+  user: userRouter,
 });

Micropostの取得/登録APIの実装

Micropostを取得/登録できるようにします。

src/server/trpc/router/micropost.ts を作成します。

import { t } from "../trpc";
import { z } from "zod";

export const micropostRouter = t.router({
  getUserMicroposts: t.procedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      return await ctx.prisma.micropost.findMany({
        where: { userId: input.userId },
      });
    }),
  createMicropost: t.procedure
    .input(
      z.object({ userId: z.string(), content: z.string().max(140).min(1) })
    )
    .mutation(async ({ ctx, input }) => {
      const user = await ctx.prisma.user.findUniqueOrThrow({
        where: { id: input.userId },
      });

      return await ctx.prisma.micropost.create({
        data: { content: input.content, user: { connect: { id: user.id } } },
      });
    }),
});
--- a/src/server/trpc/router/index.ts
+++ b/src/server/trpc/router/index.ts
@@ -2,10 +2,12 @@
 import { t } from "../trpc";
 import { exampleRouter } from "./example";
 import { authRouter } from "./auth";
 import { userRouter } from "./user";
+import { micropostRouter } from "./micropost";
 
 export const appRouter = t.router({
   example: exampleRouter,
   auth: authRouter,
   user: userRouter,
+  micropost: micropostRouter,
 });

getUserMicroposts ではあるユーザーに紐付いたmicropostsを全件取得し、 createMicropost では、Prisma の connect を使ってuserレコードに紐付けつつmicropostを作成しています。

作成時にはz.max(140) を用いて contentの長さが140字以上だとバリデーションで落ちるようにしています。また、 min(1) で1文字以上入力させます。

脚注
  1. リクエスト時にはqueryではGET、mutationではPOSTが使われます ↩︎