🧊

Next.js AppRouter + tRPC

2024/03/20に公開

概要

Next.js(v14)のAppRouterでtRPCの導入を行いたいと思います。
シンプルにNext.js + tRPCの環境を構築したい場合は「T3 Stack」のセットアップを利用するのが最速だと思います。
本内容ではtrpcの公式ドキュメントを元に、Next.jsのAppRouterベースでざっくりかんたんに環境構築を行いたいと思います。(ので個人的なメモです。)

https://trpc.io/
https://nextjs.org/
https://create.t3.gg/

ディレクトリ構成

本実装の最終的なディレクトリ構成です。
基本的に公式ドキュメント元に設置していきますが、PageRouterではなくAppRouterのため適所調整しています。

├── app
│   ├── api
│   │   └── trpc
│   │       └── [trpc]
│   │           └── route.ts    // APIエンドポイント(ルートハンドラーでのtRPCルーターを提供)
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── ClientSideComponent.tsx
│   └── ServerSideComponent.tsx
├── server                  // バックエンド側に関する処理
│   ├── index.ts                // tRPCルーターの定義
│   └── trpc.ts                 // tRPCルーターの初期化
└── trpc                    // tRPCをアプリケーションに提供する処理
    ├── client.ts               // ReactQueryのセットアップ
    └── provider.tsx            // tRPCプロバイダー

Next.jsアプリ構築

npx create-next-app@latest
  • use TypeScript? -> Yes
  • use App Router? -> Yes

https://nextjs.org/docs/getting-started/installation

tRPCパッケージのインストール

npm install @trpc/server@next @trpc/client@next @trpc/react-query@next @trpc/next@next @tanstack/react-query@latest

https://trpc.io/docs/client/nextjs/setup#1-install-deps

バックエンド(サーバー)側の設定

tRPCの初期化の設定とメインルーターのインスタンス構築を行います。

インスタンスを作成し必要なヘルパー関数をエクスポートします。

server/trpc.ts
/**
 * Initialization of tRPC backend
 * @link https://trpc.io/docs/server/routers
 */
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

/**
 * Create router
 * @link https://trpc.io/docs/v11/router
 */
export const router = t.router

/**
 * Create public procedure
 * @link https://trpc.io/docs/v11/procedures
 **/
export const procedure = t.procedure

/**
 * Create createCallerFactory
 * @link https://trpc.io/docs/v11/server/server-side-calls
 */
export const createCallerFactory = t.createCallerFactory

次にメインルーターを作成します。
ここではhelloという文字列を返却するクエリで定義しています。
今回は利用しませんがtRPCはExpressやFastifyをバックエンドとして利用できるように様々なアダプターが提供されています。

server/index.ts
import { procedure, router, createCallerFactory } from './trpc'

/**
 * router of tRPC-backend
 * @link https://trpc.io/docs/quickstart#1-create-a-router-instance
 */
export const appRouter = router({
  hello: procedure.query(() => {
    return { msg: 'Hello World' }
  }),
})

export type AppRouter = typeof appRouter

export const createCaller = createCallerFactory(appRouter)

https://trpc.io/docs/quickstart

Next.jsアダプター作成

Next.jsでtRPCルーターを提供するためのAPIハンドラーを作成します。

app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '~/server'

/**
 * tRPC's HTTP response handler
 * @link https://trpc.io/docs/server/adapters/nextjs
 */
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  })

export { handler as GET, handler as POST }

https://trpc.io/docs/server/adapters/nextjs

フロントエンドの設定

フロントエンドでクライアントコンポーネントを利用するため、ReactQueryのセットアップを行います。

AppRouterを利用するtRPCクライアントを生成します。

trpc/client.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '~/server'

/**
 * React hooks from `AppRouter`
 * @link https://trpc.io/docs/client/react/setup#2-import-your-approuter
 */
export const trpc = createTRPCReact<AppRouter>()

tRPCクライアントのプロバイダーを作成し、ReactQueryのセットアップをします。

trpc/provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import React, { ReactNode, useState } from 'react'

import { trpc } from './client'

/**
 * tRPC React-Query Provider
 * @link https://trpc.io/docs/client/react/setup#4-add-trpc-providers
 */
export default function TrpcProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({}))
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    }),
  )
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  )
}

プロバイダーを設定します。

app/layout.tsx
import type { Metadata } from 'next'
+import TrpcProvider from '~/trpc/provider'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
-       {children}
+       <TrpcProvider>{children}</TrpcProvider>
      </body>
    </html>
  )
}

https://trpc.io/docs/client/react

クライアントコンポーネント

ClientComponentを作成し、tRPC ReactQueryを利用します。

components/ClientSideComponent.tsx
'use client'

import { FC } from 'react'
import { trpc } from '~/trpc/client'

export const ClientSideComponent: FC = () => {
  const { data } = trpc.hello.useQuery()

  return (
    <div>
      <h1>Client Side Component</h1>
      <div>{JSON.stringify(data)}</div>
    </div>
  )
}

作成したコンポーネントをapp/page.tsxで読み込み動作を確認します。

app/page.tsx
import { ClientSideComponent } from '~/components/ClientSideComponent'

export default function Home() {
  return (
    <div>
      <ClientSideComponent />
    </div>
  );
}

https://trpc.io/docs/client/react/setup

サーバーコンポーネント

ServerComponentを作成し、tRPCルーターの呼び出しを行います。
Next.jsでの同じホストサーバーでの呼び出しのため、createCallerFactoryを利用します。

tRPCのメインルーター(server/index.ts)でcreateCallerFactoryから生成したcreateCallerを利用してhelloクエリを呼び出しています。

components/ServerSideComponent.tsx
import { FC } from 'react'
import { createCaller } from '~/server'

export const ServerSideComponent: FC = async () => {
  const caller = createCaller({})
  const data = await caller.hello()
  return (
    <div>
      <h1>Server Side Component</h1>
      <div>{JSON.stringify(data)}</div>
    </div>
  )
}

作成したコンポーネントをapp/page.tsxで読み込み動作を確認します。

app/page.tsx
import { ClientSideComponent } from '~/components/ClientSideComponent'
+import { ServerSideComponent } from '~/components/ServerSideComponent'

export default function Home() {
  return (
    <div>
      <ClientSideComponent />
+     <ServerSideComponent />
    </div>
  );
}

https://trpc.io/docs/server/server-side-calls

おわりに

tRPCを利用することでサーバー(バックエンド)の実装を行うだけで、型安全なAPIクライアントの実装を行うことができました。
gRPCと名前が似ているため同じような技術かと思いきや実際は大きく異なり
スキーマ作成やコード生成が不要でクライアントとサーバー間のやり取りが簡単にできてしまいます。
フルスタックなNext.jsを構築する際には非常に相性が良さそうです。
今回はAppRouterでの実装を行いたかったため、Next.jsアプリを作成し組み込みましたが
最近注目を集めている「T3 Stack」を用いて開発する方が最適かつ最速であると考えられます。

tRPCは非常に強力なツールでありますが、柔軟性やアーキテクチャに対応できない部分があります。

  • TypeScript以外の言語を利用できない
  • フロントエンドとバックエンドの開発環境が密に依存してしまう
  • APIクライアントを外部連携に提供できない

また、REST API / GraphQL / gRPCとの比較するものでもなく
プロジェクトの将来を考慮し長所を最適化するような考えが必要だと思われます。

私もこれからtPRCのさらに深い知識に触れれるよう様々な機能の実践に取り組みたいと思います。🪷

Discussion