tRPCを触ってみる
IT技術
気になっていたtRPCを実際に触ってみた記録です!
tRPCとは
公式から引用させてください。
tRPC allows you to easily build & consume fully typesafe APIs without schemas or code generation.
引用: https://trpc.io/docs/#introduction
この通りで、スキーマ等なしで、簡単に型安全なAPIを作成することができます!
ちなみに、tRPCは、TypeScriptを想定して作られているので、TypeScript以外での使用はできません><
逆に、TypeScriptを使ったプロジェクトでは、かなり有用なツールになり得ます。
サンプルアプリを構築してみる
今回はNext.jsを使って、tRPCのサンプルアプリを作ってみようと思います。
Next.jsのAPI Routesを利用して、tRPCの制限をかけたAPIを作成し、クライアント側でそのAPIを呼び出す、という形を想定しています。
また、ディレクトリ構成は、公式がおすすめしていた以下の構成をとるようにします。
1// 引用: https://trpc.io/docs/nextjs/setup#recommended-file-structure
2.
3├── prisma # <-- if prisma is added
4│ └── [..]
5├── src
6│ ├── pages
7│ │ ├── _app.tsx # <-- add `withTRPC()`-HOC here
8│ │ ├── api
9│ │ │ └── trpc
10│ │ │ └── [trpc].ts # <-- tRPC HTTP handler
11│ │ └── [..]
12│ ├── server
13│ │ ├── routers
14│ │ │ ├── _app.ts # <-- main app router
15│ │ │ ├── post.ts # <-- sub routers
16│ │ │ └── [..]
17│ │ ├── context.ts # <-- create app context
18│ │ └── trpc.ts # <-- procedure helpers
19│ └── utils
20│ └── trpc.ts # <-- your typesafe tRPC hooks
21└── [..]
Next.jsをインストール
1yarn create next-app --typescript
tRPCのモジュールをインストール
1npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
サーバー側のtRPCのセットアップ
APIを提供するサーバー側のセットアップを行なっていきます。
tRPCのインスタンス生成
tRPCを使用する上で必要なものを用意します。
1// /src/server/trpc.ts
2import { initTRPC, TRPCError } from '@trpc/server';
3
4// initTRPCはアプリ内で一度のみ使用する
5const t = initTRPC.create({
6 errorFormatter({ shape }) {
7 return shape;
8 },
9});
10
11export const router = t.router;
12
13// API毎のプロシージャのベースとなるプロシージャ。認証別など、複数のベースプロシージャを定義できる
14export const publicProcedure = t.procedure;
15
16// tRPC上のミドルウェア
17export const middleware = t.middleware;
18
19// 全てのプロシージャをフラットに1つの名前空間にする場合に使用する
20export const mergeRouters = t.mergeRouters;
ルーターを作成する
疎通確認のためのAPI、healthcheckのルートを追加してます。
1// /src/server/routers/_app.ts
2import { publicProcedure, router } from "../trpc";
3
4export const appRouter = router({
5 healthcheck: publicProcedure.query(() => 'fine!')
6});
7
8export type AppRouter = typeof appRouter;
API/APIハンドラーを追加
今回は、Next.js API Routes対象の/api/trpc/[trpc]
にエンドポイントを置きます。
1// src/pages/api/trpc/[trpc].ts
2import * as trpcNext from '@trpc/server/adapters/next';
3import { appRouter } from '@/server/routers/_app';
4
5export default trpcNext.createNextApiHandler({
6 router: appRouter,
7 onError({ error }) {
8 if (error.code === 'INTERNAL_SERVER_ERROR') {
9 console.error('Something went wrong', error);
10 }
11 },
12 batching: {
13 enabled: true,
14 },
15});
APIにアクセスしてみる
この時点で、http://localhost:3000/api/trpc/healthcheck にアクセスすると、以下のレスポンスが返ってきました!
1{
2 "result": {
3 "data": "fine!"
4 }
5}
Contextを追加してみる
tRPCにはContextという機能があり、全てのtRPCプロシージャがアクセス可能なデータを格納できます。
認証情報の共有などに便利ですが、今回はCookieを共有させてみます。
1. Contextの型定義&生成処理を作成する
1// /src/server/routers/context.ts
2import * as trpc from '@trpc/server';
3import * as trpcNext from '@trpc/server/adapters/next';
4
5interface CreateContextOptions {
6 cookie: Partial<{
7 [key: string]: string;
8 }>,
9}
10
11// context作成時の内部処理を切り出すことで、テスト時に便利になるそう
12export async function createContextInner(opts: CreateContextOptions) {
13 return {
14 cookie: opts.cookie
15 };
16}
17
18export type Context = trpc.inferAsyncReturnType<typeof createContextInner>;
19
20export async function createContext(
21 opts: trpcNext.CreateNextContextOptions,
22): Promise<Context> {
23 return await createContextInner({
24 cookie: opts.req.cookies
25 });
26}
2. initTRPCに連携する
1// /src/server/trpc.ts
2// ...
3import { Context } from './context';
4
5const t = initTRPC.context<Context>().create({
6 errorFormatter({ shape }) {
7 return shape;
8 },
9});
10
11// ...
3. 試しに取得してみる
ここではmiddleware内で取得していますが、プロシージャ内でも取得できます。
1// /src/server/trpc.ts
2// ...
3
4const isAuthed = t.middleware(({ next, ctx }) => {
5 // contextからcookieを取り出す
6 if (!ctx.cookie['test-cookie']) {
7 throw new TRPCError({
8 code: 'UNAUTHORIZED',
9 });
10 }
11 return next({
12 ctx: {
13 cookie: ctx.cookie
14 },
15 });
16});
17
18export const protectedProcedure = t.procedure.use(isAuthed) // このベースプロシージャを適用すると、isAuthedがtrueの場合のアクセスのみ許可できる
19
20// ...
クライアント側のtRPCのセットアップ
_app.tsxの変更
tRPCの高階コンポーネントをAppに適用します。
1import '@/styles/globals.css'
2import { trpc } from '@/utils/trpc'
3import type { AppProps } from 'next/app'
4
5function App({ Component, pageProps }: AppProps) {
6 return <Component {...pageProps} />
7}
8
9export default trpc.withTRPC(App)
tRPCのhookを用意する
1// /src/utils/trpc.ts
2import type { AppRouter } from '@/server/routers/_app';
3import { httpBatchLink } from '@trpc/client';
4import { createTRPCNext } from '@trpc/next';
5
6function getBaseUrl() {
7 if (typeof window !== 'undefined')
8 // ブラウザでは相対パスを利用
9 return '';
10
11 return `http://localhost:${process.env.PORT ?? 3000}`;
12}
13
14export const trpc = createTRPCNext<AppRouter>({
15 config() {
16 return {
17 links: [
18 httpBatchLink({
19 url: `${getBaseUrl()}/api/trpc`,
20
21 // ヘッダー操作などはここでできる
22 // async headers() {
23 // return {
24 // authorization: 'token',
25 // };
26 // },
27 }),
28 ],
29 };
30 },
31 ssr: typeof window === 'undefined',
32});
APIリクエストしてみる
先ほど確認用のルート(healthcheck)を追加したので、クライアントから呼び出してみます。
1// /src/pages/index.tsx
2import { trpc } from '@/utils/trpc'
3
4export default function Home() {
5 const { data, isLoading} = trpc.healthcheck.useQuery()
6 return (
7 <>
8 {isLoading && 'loading...'}
9 {data}
10 </>
11 )
12}
http://localhost:3000/ にアクセスしてみると、無事に取得したデータが表示されていました!
セットアップ完了
これでクライアントと、tRPCベースのAPIの疎通もできました!
意外と簡単に構築でき、びっくりです。
APIのinputの型付け
tRPCといえば、APIの型付けができる点が魅力ポイントなので、やってみます!
サーバー側のルート追加
サブルーターを作る
今回追加するルートに当たるサブルーターを用意します。
今回はお試しなので、数字を受け取って、その数字の分だけ1
を返すAPIを作ります。
inputとしては、1~50までのnumber
値 count
を受け取ります。
1// /src/server/routers/sample.ts
2import { router, publicProcedure } from '../trpc';
3import { z } from 'zod';
4
5export const sampleRouter = router({
6 repeat: publicProcedure
7 // ここでinputの型指定
8 .input(
9 z.object({
10 count: z.number().min(1).max(50),
11 }),
12 )
13 .query(async ({ input }) => {
14 const items = []
15 for (let i = 0; i < input.count; i++) {
16 items.push('1')
17 }
18
19 return { items }
20 }),
21});
今回はバリデーションライブラリとして、Zodを使用していますが、YupやSuperstructも使用できます。
サブルーターを全体ルーターに組み込み
作成したサブルーターを全体ルーターに組み込みます。
1// /src/server/routers/_app.ts
2import { protectedProcedure, publicProcedure, router } from "../trpc";
3import { sampleRouter } from "./sample";
4
5export const appRouter = router({
6 healthcheck: publicProcedure.query(() => 'fine!'),
7 sample: sampleRouter // 追加
8});
9
10// Export also type router type signature,
11export type AppRouter = typeof appRouter;
クライアント側で呼び出す
APIリクエストする
クライアント側から呼び出します。
1// /src/pages/index.tsx
2import { trpc } from '@/utils/trpc'
3
4export default function Home() {
5 const { data: healthCheckData} = trpc.healthcheck.useQuery()
6 const { data: repeatData } = trpc.sample.repeat.useQuery({ count: 20 }) // 追加
7
8 return (
9 <>
10 {healthCheckData}
11 {repeatData?.items.map((item) => item)}
12 </>
13 )
14}
通信を確認してみる
今回、最初に作ったヘルスチェックのクエリと、新しく作ったリピートクエリを2つ呼び出していましたが、エンドポイントも1つになって呼び出されています!
ちなみに、複数のAPIがまとめてコールされているのは、httpBatchLinkやbatchingの設定をバッチ処理するように指定しているからです。
もちろん、個別に実行したい場合は、設定を変更すればOKです。
指定したinputの型以外を送信してみる
1~50の範囲外の値をinputに入れてみました。
結果は、エラー(400 Bad Request)となり、適合しないinputでは通信が失敗するようになっています!
1// response
2[
3 {
4 error: {
5 message:
6 '[\n {\n "code": "too_big",\n "maximum": 50,\n "type": "number",\n "inclusive": true,\n "exact": false,\n "message": "Number must be less than or equal to 50",\n "path": [\n "count"\n ]\n }\n]',
7 code: -32600,
8 data: {
9 code: 'BAD_REQUEST',
10 httpStatus: 400,
11 stack: 'TRPCError: ...',
12 path: 'sample.repeat',
13 },
14 },
15 },
16]
outputの型付け
レスポンスに当たるoutputの型付けも以下のように簡単にできます。
1// /src/server/routers/sample.ts
2import { router, publicProcedure } from '../trpc';
3import { z } from 'zod';
4
5export const sampleRouter = router({
6 repeat: publicProcedure
7 .input(
8 z.object({
9 count: z.number().min(1).max(50),
10 }),
11 )
12 // アウトプットのバリデーション追加
13 .output(
14 z.object({
15 items: z.string().array(),
16 }),
17 )
18 .query(async ({ input }) => {
19 const items = []
20 for (let i = 0; i < input.count; i++) {
21 items.push('1')
22 }
23
24 return { items }
25 }),
26}),
ちなみに、アウトプットのバリデーションに引っかかった場合は、デフォルトでは以下のようなレスポンスが返ってきました。
1// response
2[
3 {
4 error: {
5 message: 'Output validation failed',
6 code: -32603,
7 data: {
8 code: 'INTERNAL_SERVER_ERROR',
9 httpStatus: 500,
10 stack: 'TRPCError: Output validation failed ...',
11 path: 'sample.repeat',
12 },
13 },
14 },
15]
まとめ
今回は、tRPCの基本的な部分を触ってみました!
今回はNext.jsでしたが、他の技術でも利用可能です。公式でボイラープレートやアダプターなど公開されているので、ぜひチェックしてみてください。
→ https://trpc.io/docs/awesome-trpc
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
Webエンジニア