フロントエンド開発の先端を突っ走るNext.js
next.js アプリの初期化( npx create-next-app@latest <アプリ名> ) または yarn create next-app <アプリ名> または bun create next-app <アプリ名> )

Next.jsのApp routerでtRPCサーバを立てて、サーバーサイド、クライアントサイドから利用する

● ファイル構成

next.js の appルーターでエンドポイントを1つ作って、あとは trpc の中で分岐させるので作るのもはがすのも簡単です。

├── app
│       └── api
│               └── trpc
│                   └── [trpc]
│                       └── route.ts (エンドポイント /api/trpc/ を処理する next.js app ルーターの route.ts)
└── trpc
        ├── client
        │       ├── TrpcProvider.tsx
        │       ├── client.ts
        │       └── serverSideClient.ts
        └── server
            ├── context.ts
            ├── routers
            │       ├── index.ts
            │       ├── userRouter.ts (ユーザーに関するルーティング)
            │       └── postRouter.ts (投稿に関するルーティング)
            └── trpc.ts

● npmパッケージのインストール

npm i @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^4.0.0 zod
npm i -D @tanstack/react-query-devtools@4.35.0

● server

src/trpc/server/trpc.ts

import { initTRPC } from "@trpc/server";

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

src/trpc/server/routers/index.ts

import { z } from "zod";

import { publicProcedure, router } from "../trpc";

export const appRouter = router({
  helloUser: publicProcedure
    .input(
      z.object({
        userName: z.string(),
      }),
    )
    .mutation(async (opts) => {
      return { text: `Hello ${opts.input.userName}` };
    }),
  hello: publicProcedure.query(async () => {
    return { text: "Hello" };
  }),
  helloText: publicProcedure
    .input(
      z.object({
        text: z.string(),
      })
    )
    .query(async (opts) => {
      return { text: `Hello ${opts.input.text}` };
    }),
});

export type AppRouter = typeof appRouter;

いわゆる GET は.query() POST は .mutation() になります。

● client

src/trpc/client/client.ts

trpc は reactQuery で使用します。
trpcClient は await を行いたい時など直接リクエストする時に使用します。

import {
  createTRPCProxyClient,
  createTRPCReact,
  httpBatchLink,
} from "@trpc/react-query";

import { type AppRouter } from "../server/routers";

export const trpc = createTRPCReact<AppRouter>({});

export const trpcClient = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: `${process.env.NEXT_PUBLIC_BACKEND_API_BASE_URL}/api/trpc`,
    }),
  ],
});

src/trpc/client/serverSideClient.ts

import { httpBatchLink } from "@trpc/client";

import { appRouter } from "../server";

export const serverSideClient = appRouter.createCaller({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/api/trpc",
    }),
  ],
});

src/trpc/client/TrpcProvider.tsx

"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import React, { useState } from "react";

import { trpc } from "./client";

export default function TrpcProvider({
  children,
}: {
  children: React.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>
  );
}

● TrpcProvide を設定する

src/app/layout.tsx

TrpcProvider を以下のように追加します

import TrpcProvider from "@/trpc/client/TrpcProvider";

  return (
    <html lang="en">
      <body className={inter.className}>
        <TrpcProvider>{children}</TrpcProvider>
      </body>
    </html>
  );

● クライアントサイドで取得する(React Query)

クライアントサイドでAPI hello を叩くには trpc.hello.useQuery() を実行します。

src/features/hello/Hello.tsx

"use client";

import { FC } from "react";
import { trpc } from "@/trpc/client/client";

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

  return (
    <div>
      <h1>HelloComponent</h1>
      <div>{JSON.stringify(data)}</div>
    </div>
  );
};

引数を受け取る helloText はこのようにして呼び出します。

  const { data } = trpc.helloText.useQuery({ text: 'ユーザー名' });

コンポーネントマウント時ではなく、ステートに何か値が入った時にリクエストする場合は次のようにします

  const [text, setText] = useState<string>("")
  const { data } = trpc.helloText.useQuery(
    { text: text },
    {
      enabled: text !== "", // text が "" でない場合にのみクエリを有効にする
    },
  )

// 任意のタイミングで setText() して text に値をセットすると、APIコール発火

● クライアントサイドで取得する(直接)

どうしても await したい時はこちらの方法も有効です。

import { trpcClient } from "@/trpc/client/client"

const result = await trpcClient.hello({ text:'直接取得' })

● サーバーサイドで取得する

サーバーサイドでAPI helloUser を叩くには await serverSideClient.helloUser() を実行します。

src/app/hello-server/page.tsx

import { serverSideClient } from "@/trpc/client/serverSideClient";

const HelloServerPage = async () => {
  const data = await serverSideClient.helloUser({
    userName: "テスト太郎",
  });

  return <>{data.text}</>;
};

export default HelloServerPage;

● サーバーサイドとクライアントサイドの取得の違い

例えば以下のように 独自クラスUser を返す trpcエンドポイントを作成したとします。

  getUser: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return new User({
        id: "1",
        name: input.name,
      });
    }),

・サーバーサイドで取得した場合は独自クラスがそのまま返されます

console.log(user)

(updatedAt は Dateオブジェクト)

User {
  id: '1',
  name: 'server side',
  updatedAt: 2024-01-23T05:53:31.525Z
}

・クライアントサイドで取得した場合は独自クラスが普通のオブジェクトに変換されて返されます

console.log(user)

(updatedAt は 文字列)

{
    "id": "1",
    "name": "hoge fuga",
    "updatedAt": "2024-01-23T06:01:58.341Z"
}

● jwtトークン認証する

https://trpc.io/docs/server/authorization#create-context-from-request-headers

次の手順で認証済みルートを作成します。

- クライアント側で、ログインしていれば { Authorization: `Bearer ${token}` } をしていなければ {} ヘッダを送信するようにします。
- サーバー側で、受け取ったトークンを検証します。(firebase-admin など)
- 「誰でもアクセス可能な publicProcedure」「ログイン済みユーザーのみアクセス可能な protectedProcedure」を作成します。
- 制限をかけたいルートは protectedProcedure を使ってルーティングを定義します。

● エンドポイントの階層化 (サブルーター)

/api/trpc/user.hello を定義してみます。

import { router, publicProcedure } from '@trpc/server';

// Userサブルーター
const userRouter = router({
  hello: publicProcedure.query(async () => {
    return { text: "Hello from user" };
  }),
});

// メインアプリケーションルーター
export const appRouter = router({
  user: userRouter, // userサブルーターを追加
  myInfo: publicProcedure.query(async () => {
    return { text: "myInfo" };
  }),
});

https://trpc.io/docs/server/merging-routers

● エンドポイントに rate limit を設定する

https://github.com/OrJDev/trpc-limiter/tree/main/packages/memory

● react__WEBPACK_IMPORTED_MODULE_3__.createContext) is not a function エラーが出る場合

もしかすると 'user client' つけ忘れかもしれないので確認しましょう。

参考 : tRPCの便利だった機能
ReactでtRPCのミドルウェアっぽいものを作る

添付ファイル1
No.2422
02/17 21:59

edit

添付ファイル