Closed24

Next.js 14 で tRPC 10 をセットアップする

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Next.js プロジェクト作成

コマンド
npx create-next-app@latest \
  --typescript \
  --tailwind \
  --eslint \
  --src-dir \
  --import-alias "@/*" \
  --use-npm \
  nextjs-trpc-setup
cd nextjs-trpc-setup

Would you like to use App Router? には No と答える。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

tRPC 関連ファイル作成

コマンド
mkdir -p src/pages/api/trpc
touch src/pages/api/trpc/\[trpc\].ts
mkdir -p src/server/routers
touch src/server/routers/_app.ts
touch src/server/routers/post.ts
touch src/server/context.ts
touch src/server/trpc.ts
mkdir -p src/utils
touch src/utils/trpc.ts

作成したファイルは下記の 6 点。

  • src/pages/api/trpc/[trpc].ts
  • src/server/routers/_app.ts
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts
  • src/utils/trpc.ts
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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

コマンド
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^4.0.0 zod
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

tsconfig.json の strict モードの確認

compilerOptions.strict が true になっていることを確認する。

tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

これは Zod を入力バリデーションに使用するために必要なようだ。

https://www.typescriptlang.org/tsconfig#strict

これを true にすると色々な strictXXX オプションを全て true になるようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

server/trpc.ts のコーディング

  • src/pages/api/trpc/[trpc].ts
  • src/server/routers/_app.ts
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts ← これ
  • src/utils/trpc.ts
src/server/trpc.ts
import { initTRPC } from "@trpc/server";

const t = initTRPC.create();

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

ドキュメントだと export const publicProcedure = t.procedure; になっているが別ページを見ると publicProcedure の方が良さそうなのでこちらを採用した。

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

https://trpc.io/docs/server/procedures

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

server/routers/_app.ts のコーディング

  • src/pages/api/trpc/[trpc].ts
  • src/server/routers/_app.ts ← これ
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts
src/server/routers/_app.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";

export const appRouter = router({
  hello: publicProcedure
    .input(
      z.object({
        text: z.string(),
      })
    )
    .query((opts) => {
      return {
        greeting: `hello ${opts.input.text}`,
      };
    }),
});

export type AppRouter = typeof appRouter;
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pages/api/trpc/[trpc].ts のコーディング

  • src/pages/api/trpc/[trpc].ts ← これ
  • src/server/routers/_app.ts ← 完了
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts
src/pages/api/trpc/[trpc].ts
import { appRouter } from "@/server/routers/_app";
import { createNextApiHandler } from "@trpc/server/adapters/next";

export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

公式ドキュメントの書き方は下記の通りなのだが trpcNext が入るのはムダな気がする。

src/pages/api/trpc/[trpc].ts
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';

// export API handler
// @see https://trpc.io/docs/server/adapters
export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

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

上のページでは import * as trpcNext は使われていない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

utils/trpc.ts のコーディング

  • src/pages/api/trpc/[trpc].ts ← 完了
  • src/server/routers/_app.ts ← 完了
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts ← これ
src/utils/trpc.ts
import type { AppRouter } from "@/server/routers/_app";
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";

export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      links: [
        httpBatchLink({
          url: "/api/trpc",
        }),
      ],
    };
  },
});

https://trpc.io/docs/client/nextjs/setup#4-create-trpc-hooks

公式ドキュメント通りだともっと長いですが SSR を使わない場合は上記だけでも大丈夫そう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

現在の状態

  • src/pages/api/trpc/[trpc].ts ← 完了
  • src/server/routers/_app.ts ← 完了
  • src/server/routers/post.ts
  • src/server/context.ts
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts ← 完了

まだ完了していないファイルがあるが後からやっていくことにしよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pages/_app.tsx の編集

src/pages/_app.tsx
import "@/styles/globals.css";
import { trpc } from "@/utils/trpc";
import type { AppProps, AppType } from "next/app";

const App: AppType = ({ Component, pageProps }) => {
  return <Component {...pageProps} />;
};

export default trpc.withTRPC(App);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

pages/index.tsx の編集

src/pages/index.tsx
import { trpc } from "@/utils/trpc";

export default function Home() {
  const { isLoading, data } = trpc.hello.useQuery({ text: "text" });

  return (
    <main className="container mx-auto px-4">
      <h1 className="mt-4 mb-4 text-2xl">Next.js + tRPC Setup</h1>
      {isLoading ? (
        <p>Loading...</p>
      ) : (
        <dl className="mb-4">
          <dt className="font-bold">Result</dt>
          <dd>{data?.greeting}</dd>
        </dl>
      )}
    </main>
  );
}
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;


実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

解決は難しそう

どうしても Suspense を使わなければならない理由がない限りは useSuspenseQuery を使うのは控えよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

server/routers/post.ts のコーディング

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

  • src/pages/api/trpc/[trpc].ts ← 完了
  • src/server/routers/_app.ts ← 完了
  • src/server/routers/post.ts ← これ
  • src/server/context.ts
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts ← 完了
src/server/routers/post.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";

export const postRouter = router({
  get: publicProcedure
    .input(
      z.object({
        id: z.number(),
      })
    )
    .query((opt) => {
      return {
        id: opt.input.id,
        content: "content",
      };
    }),
});
src/server/routers/_app.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";
import { postRouter } from "./post";

export const appRouter = router({
  hello: publicProcedure
    .input(
      z.object({
        text: z.string(),
      })
    )
    .query(async (opts) => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return {
        greeting: `hello ${opts.input.text}`,
      };
    }),
  post: postRouter,
});

export type AppRouter = typeof appRouter;
src/pages/index.tsx
import { trpc } from "@/utils/trpc";
import { FC } from "react";

export default function Home() {
  return (
    <main className="container mx-auto px-4">
      <h1 className="mt-4 mb-4 text-2xl">Next.js + tRPC Setup</h1>
      <Result></Result>
    </main>
  );
}

const Result: FC = () => {
  const { isLoading, data } = trpc.post.get.useQuery({ id: 1 });

  if (isLoading || !data) {
    return <p>Loading...</p>;
  }

  return (
    <dl className="mb-4">
      <dt className="font-bold">Post ID</dt>
      <dd className="mb-2">{data.id}</dd>
      <dt className="font-bold">Content</dt>
      <dd>{data.content}</dd>
    </dl>
  );
};


実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

server/context.ts のコーディング

https://trpc.io/docs/server/context

  • src/pages/api/trpc/[trpc].ts ← 完了
  • src/server/routers/_app.ts ← 完了
  • src/server/routers/post.ts ← 完了
  • src/server/context.ts ← これ
  • src/server/trpc.ts ← 完了
  • src/utils/trpc.ts ← 完了
src/server/context.ts
import { CreateNextContextOptions } from "@trpc/server/adapters/next";

export const createContext = async (opts: CreateNextContextOptions) => {
  const session = { userId: "userId" };
  return { session };
};

export type Context = Awaited<ReturnType<typeof createContext>>;
src/server/trpc.ts
import { initTRPC } from "@trpc/server";
import { Context } from "./context";

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
src/pages/api/trpc/[trpc].ts
import { createContext } from "@/server/context";
import { appRouter } from "@/server/routers/_app";
import { createNextApiHandler } from "@trpc/server/adapters/next";

export default createNextApiHandler({
  router: appRouter,
  createContext,
});
src/server/routers/post.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";

export const postRouter = router({
  get: publicProcedure
    .input(
      z.object({
        id: z.number(),
      })
    )
    .query((opt) => {
      return {
        id: opt.input.id,
        content: "content",
        userId: opt.ctx.session.userId,
      };
    }),
});
src/pages/index.tsx
import { trpc } from "@/utils/trpc";
import { FC } from "react";

export default function Home() {
  return (
    <main className="container mx-auto px-4">
      <h1 className="mt-4 mb-4 text-2xl">Next.js + tRPC Setup</h1>
      <Result></Result>
    </main>
  );
}

const Result: FC = () => {
  const { isLoading, data } = trpc.post.get.useQuery({ id: 1 });

  if (isLoading || !data) {
    return <p>Loading...</p>;
  }

  return (
    <dl className="mb-4">
      <dt className="font-bold">Post ID</dt>
      <dd className="mb-2">{data.id}</dd>
      <dt className="font-bold">Content</dt>
      <dd className="mb-2">{data.content}</dd>
      <dt className="font-bold">User ID</dt>
      <dd className="mb-2">{data.userId}</dd>
    </dl>
  );
};
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

昨日は急いでいたのでコピペで済ませてしまったが、今回は手打ちでじっくり学べて良かった。

今度はユーザー認証と組み合わせる方法について学んでみたい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

最小限セットアップ

コマンド
npx create-next-app@latest \
  --typescript \
  --tailwind \
  --eslint \
  --src-dir \
  --import-alias "@/*" \
  --use-npm \
  nextjs-trpc-minimal-setup
cd nextjs-trpc-minimal-setup

npm install @trpc/server @trpc/client @trpc/next zod

mkdir -p src/pages/api/trpc
touch src/pages/api/trpc/\[trpc\].ts
src/pages/api/trpc/[trpc].ts
import { initTRPC } from "@trpc/server";
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { z } from "zod";

const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
const appRouter = router({
  greeting: publicProcedure
    .input(
      z.object({
        name: z.string(),
      })
    )
    .query((opt) => {
      return {
        text: `Hello ${opt.input.name}`,
      };
    }),
});

export type AppRouter = typeof appRouter;
export default createNextApiHandler({ router: appRouter });
src/pages/index.tsx
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import Head from 'next/head'
import type { AppRouter } from './api/trpc/[trpc]'
import { useEffect, useState } from 'react'

const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/api/trpc",
    }),
  ]
})

export default function Home() {
  const [text, setText] = useState('')

  useEffect(() => {
    trpc.greeting.query({ name: 'name' })
      .then(({ text }) => setText(text))
  }, [])

  return (
    <main className="container mx-auto p-4">
      <Head>
        <title>Next.js + tRPC Minimal Setup</title>
      </Head>
      <h1 className="mt-4 mb-4 text-2xl">Next.js + tRPC Minimal Setup</h1>
      <dl className="mb-4">
        <dt className="font-bold">Result</dt>
        <dd className="mb-2">{text}</dd>
      </dl>
    </main>
  )
}


実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

SWR を使う場合

コマンド
npm install swr
src/pages/index.tsx
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import Head from 'next/head'
import type { AppRouter } from './api/trpc/[trpc]'
import useSWR from 'swr'

const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/api/trpc",
    }),
  ]
})

export default function Home() {
  const { data } = useSWR('greeting', () => trpc.greeting.query({ name: 'name' }))

  return (
    <main className="container mx-auto p-4">
      <Head>
        <title>Next.js + tRPC Minimal Setup</title>
      </Head>
      <h1 className="mt-4 mb-4 text-2xl">Next.js + tRPC Minimal Setup</h1>
      {!data ? <p className="mb-4">Loading...</p> : (<dl className="mb-4">
        <dt className="font-bold">Result</dt>
        <dd className="mb-2">{data.text}</dd>
      </dl>)}
    </main>
  )
}
このスクラップは4ヶ月前にクローズされました