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 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
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() になります。
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>
);
}
src/app/layout.tsx
TrpcProvider を以下のように追加します
import TrpcProvider from "@/trpc/client/TrpcProvider";
return (
<html lang="en">
<body className={inter.className}>
<TrpcProvider>{children}</TrpcProvider>
</body>
</html>
);
クライアントサイドで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"
}
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
https://github.com/OrJDev/trpc-limiter/tree/main/packages/memory
もしかすると 'user client' つけ忘れかもしれないので確認しましょう。