Next.js + tRPCをApp Routerで使う

この記事では、 Next.js と tRPC を使ってアプリケーションをセットアップする方法について解説します。クライアントコンポーネントとサーバーコンポーネントの両方で tRPC を活用する方法を紹介します。

## 環境構築

### Next.jsのセットアップ

```
npx create-next-app@latest
```

### 必要なパッケージのインストール
```
npm i @trpc/server@next @trpc/client@next swr
```
ここでは、tRPC v11（プレリリース版）と、クライアント側のデータ取得に SWR を使用します。

## tRPCサーバーの実装

基本的には、[tRPC Quickstart](https://trpc.io/docs/quickstart) にそって行います。


### tRPCルーターの作成

```ts:server/trpc.ts
import { initTRPC } from '@trpc/server';
 
/**
 * Initialization of tRPC backend
 * Should be done only once per backend!
 */
const t = initTRPC.create();
 
/**
 * Export reusable router and procedure helpers
 * that can be used throughout the router
 */
export const router = t.router;
export const publicProcedure = t.procedure;
```

### API エンドポイントの定義
サンプルとして getUser 関数を定義します。

```ts:server/index.ts
import { setTimeout } from "node:timers/promises";
import { publicProcedure, router } from "./trpc";

const getUser = async () => {
  // mock delay
  await setTimeout(1000);
  return { name: "User" };
};

export const appRouter = router({
  getUser: publicProcedure.query(async () => {
    const users = await getUser();
    return users;
  }),
});

// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
```

### Route Handlersの作成

Next.jsのRoute Handlers機能を用いて、以下の handler を定義します。

```ts:app/api/trpc/[trpc]/route.ts
import { appRouter } from "@/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

function handler(req: Request) {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => ({}),
  });
}
export { handler as GET, handler as POST };
```

### クライアントコンポーネントでの tRPC 呼び出し

tRPC クライアントの作成を行います。

```ts:components/client.ts
import { AppRouter } from "@/server";
import { createTRPCClient, httpBatchLink } from "@trpc/client";

export const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/api/trpc",
    }),
  ],
});
```

呼び出しを行うクライアントコンポーネントを定義します。

```tsx:components/UserClient.tsx
"use client";

import { client } from "./client";
import useSWR from "swr";

export default function UserClient() {
  const { data } = useSWR("userList", client.getUser.query);

  if (data === undefined) {
    return <span>Loading...</span>;
  }

  return <span>UserClient: {data.name}</span>;
}
```

### サーバーコンポーネントでの tRPC 呼び出し

tRPC クライアントを作成します。
サーバー側から直接呼び出す場合は、Request を通さずに実行するため、独自の Context を作成する必要があります。

```tsx:components/server.ts
import { appRouter } from "@/server";

const createContext = () => ({});

export const server = appRouter.createCaller(createContext());
```

呼び出しを行うサーバーコンポーネントを定義します。

```tsx:components/UserServer.tsx
import { Suspense } from "react";
import { server } from "./server";

export default async function UserServer() {
  return (
    <Suspense fallback={<span>Loading...</span>}>
      <AsyncComponent />
    </Suspense>
  );
}

async function AsyncComponent() {
  const data = await server.getUser();

  return <span>UserServer: {data.name}</span>;
}
```

### コンポーネントを表示するページの実装

`UserClient`、`UserServer`を呼び出すためのページを作成します。

```tsx:app/page.tsx
import styles from "./page.module.css";
import UserClient from "./components/UserClient";
import UserServer from "./components/UserServer";

export default function Home() {
  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <UserClient />
        <UserServer />
      </main>
    </div>
  );
}
```

### 実行してみる

開発サーバーを起動します。

```
npm run dev
```

どちらもローディング中にはLoading...と表示され、

![loading](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322007/770eb469-bb55-4e0e-bc16-135a3fc11132.png)

ローディング後には値が表示されました！

![loaded](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322007/77ce593b-4863-4866-a443-619966186a8e.png)

## まとめ

この記事では、Next.js と tRPC を使って API を構築する基本的な流れを紹介しました。  
セットアップからサーバー・クライアントの実装、コンポーネントでのデータ取得まで、一通りの使い方を見てきました。  

tRPC を使えば、API の型安全を保ちつつ、シンプルな開発が可能になります。
Next.js との相性もよく、面倒なエンドポイントの定義や API の型定義を最小限に抑えられます。