首页
/ 在 Next.js 中接入 tRPC:Pages Router 下端到端类型安全的完整集成指南(trpc v9)

在 Next.js 中接入 tRPC:Pages Router 下端到端类型安全的完整集成指南(trpc v9)

2026-09-08 11:01:03作者:宣利权Counsellor

tRPC 官方文档中有一句著名的论断——"tRPC 与 Next.js 是天作之合":Next.js 允许你在同一个代码库中同时编写客户端与服务端,从而让两端共享同一套 TypeScript 类型;而 tRPC 在此基础上进一步提供了 @trpc/next 适配层与 withTRPC 高阶组件,让"零手工 API 定义、全链路类型安全"真正落地。本文依据本仓库归档的 www/versioned_docs/version-9.x/nextjs/introduction.md 文档整理,完整讲解在 Next.js(Pages Router 时代)项目中从零接入 tRPC 的推荐目录结构、六步实操流程,以及 withTRPC() 各配置项的语义与底层实现原理。读完你将能够独立把一个已有的 Next.js 项目改造成端到端类型安全的 tRPC 应用,并理解 pages/api/trpc/[trpc].ts 这类"全能路由"背后的运作机制。

版本说明:本文对应的文档归档于 www/versioned_docs/version-9.x/,其技术栈为 tRPC v9/v10 时代的 @trpc/react + React Query v3 + Next.js Pages Router(即 pages/ 目录、getServerSideProps 等 API)。仓库主线的 API 形态已持续演进,例如现代版 withTRPC 的实现位于 packages/next/src/withTRPC.tsx,但本文所述的路由组织思想、createNextApiHandler 适配器设计在当前仓库中依然保留(见 packages/server/src/adapters/next.ts),新旧读者均可对照理解。

为什么说 tRPC 与 Next.js 是"天作之合"

传统前后端协作模式下,前后端代码分开维护,接口契约靠手写文档或人工对齐,类型常常在某一端"失真"。tRPC 的思路完全不同:

  • 单一代码库:Next.js 天然支持在同一个项目里同时编写 pages/api 服务端接口与 pages/ 下的 React 页面,客户端和服务端位于同一个 TypeScript 工程中。
  • 类型零拷贝共享:服务端 router 的类型通过 typeof 直接导出为 AppRouter,客户端据此自动推导出每个 procedure 的入参与出参类型,改一处即可全链路生效。
  • 专用集成工具:tRPC 为 Next.js 提供了专门的开发体验支持,主要包括 @trpc/next 包中的两个核心能力:
    • createNextApiHandler:把 tRPC router 包装成一个 Next.js API Route handler;
    • withTRPC:一个高阶组件(HOC),在 _app.tsx 中注入类型安全的 hooks 及 React Query 客户端。

推荐的目录结构

文档给出了"推荐但不强制"的目录结构,这也是从官方示例工程起步后得到的形态。它把"路由定义""context 创建""hooks 导出"三者清晰分层:

.
├── prisma # <-- if prisma is added
│   └── [..]
├── src
│   ├── pages
│   │   ├── _app.tsx # <-- add `withTRPC()`-HOC here
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts # <-- tRPC HTTP handler
│   │   └── [..]
│   ├── server
│   │   ├── routers
│   │   │   ├── app.ts   # <-- main app router
│   │   │   ├── post.ts  # <-- sub routers
│   │   │   └── [..]
│   │   ├── context.ts      # <-- create app context
│   │   └── createRouter.ts # <-- router helper
│   └── utils
│       └── trpc.ts  # <-- your typesafe tRPC hooks
└── [..]

各文件职责说明:

文件 职责
pages/_app.tsx 全局应用入口,用 withTRPC() 高阶组件包裹 MyApp,为所有页面注入 tRPC 客户端与 React Query Provider
pages/api/trpc/[trpc].ts tRPC 的"唯一 HTTP 入口",使用 createNextApiHandler 将 router 暴露为 API Route
server/routers/*.ts 按业务领域拆分的子路由(如 post.ts),再在 app.ts 中合并成根路由 appRouter
server/context.ts 创建每个请求的应用级 context,可携带用户信息、数据库连接等
server/createRouter.ts 一个"带 context 的 router 工厂",保证所有子路由使用同一类型上下文
utils/trpc.ts AppRouter 类型参数创建类型安全的 React hooks 集合

关于该结构中 [trpc].ts 的命名为何是带方括号的动态路由:tRPC 需要把所有 procedure 请求(hellopost.listpost.create 等)都路由到同一个处理函数,由适配器根据 URL 中的路径段还原出 procedure 的完整路径。这一点在 packages/server/src/adapters/next.tscreateNextApiHandler 实现中体现得很直白:它从 req.query['trpc'] 取出路径段,若为数组则用 '/' 连接成完整路径,再交给 nodeHTTPRequestHandler 执行;当路径缺失时抛出的错误信息也明确写着 is the file named [trpc].ts or [...trpc].ts?——这意味着你既可以只用一层 [trpc].ts,也可以用 [...trpc].ts 捕获带 / 的多级路径。

分步集成:为已有 Next.js 项目接入 tRPC

第 1 步:安装依赖

yarn add @trpc/client @trpc/server @trpc/react @trpc/next zod react-query@3

需要理解两个关键点:

  • React Query 是必需的 peer dependency@trpc/react 本质上是 @tanstack/react-query 的一层薄封装,useQueryuseMutation 等 hooks 的缓存、重试、失效机制全部由 React Query 提供,因此 v9 时代要求安装 react-query@3(React Query v3)。
  • Zod 并非强制:绝大多数示例使用 Zod 做输入校验,tRPC 官方也强烈推荐,但你可以自由选择 Yup、Superstruct、io-ts 等任意校验库。事实上,任何包含 parsecreatevalidateSync 方法的对象都能作为输入校验器使用。

第 2 步:开启 TypeScript 严格模式

如果你想用 Zod 做输入校验,请确认 tsconfig.json 已开启严格模式:

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

如果完整 strict 对你现有代码库改动过大,至少也要开启 strictNullChecks

// tsconfig.json
{
  // ...
  "compilerOptions": {
    // ...
    "strictNullChecks": true
  }
}

类型安全是 tRPC 的立身之本,Nullable 相关的推断(例如示例中 z.string().nullish() 的输入)依赖严格的空值检查才能正确工作。

第 3 步:创建 tRPC router

将你的 tRPC router 实现在 ./pages/api/trpc/[trpc].ts。如果你的路由需要拆分到多个子路由,请把它们放在项目根目录顶层的 server 目录中,再引入到 ./pages/api/trpc/[trpc].ts 中,通过 merge 合并为一个根 appRouter(参见 Merging Routers)。

一个最小可用的示例 router:

import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { z } from 'zod';

export const appRouter = trpc.router().query('hello', {
  input: z
    .object({
      text: z.string().nullish(),
    })
    .nullish(),
  resolve({ input }) {
    return {
      greeting: `hello ${input?.text ?? 'world'}`,
    };
  },
});

// export type definition of API
export type AppRouter = typeof appRouter;

// export API handler
export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: () => null,
});

拆分子路由时,典型做法是让每个领域模块导出自己的 router,再用前缀合并,例如在根 router 上 .merge('user.', users).merge('post.', posts),从而在客户端得到 post.createuser.list 这样带命名空间的调用路径(详见 Merging Routers 示例代码)。若你需要在 resolver 中访问用户信息等请求级数据,则应实现 context,把 createContext() => null 替换为真正创建上下文并传给 router 工厂(trpc.router<Context>())的函数。

第 4 步:创建类型安全的 hooks

利用 AppRouter 的类型签名,创建一套强类型 hooks:

import { createReactQueryHooks } from '@trpc/react';
import type { AppRouter } from '../pages/api/trpc/[trpc]';

export const trpc = createReactQueryHooks<AppRouter>();
// => { useQuery: ..., useMutation: ...}

此后项目中的所有组件都从 utils/trpc.ts 导入 trpc 对象,而不是直接 import React Query。这样 trpc.useQuery(...) 的 path 参数会获得自动补全,input/output 的类型会被精确推断——你在服务端把 hello 的返回结构从 { greeting: string } 改成别的形状,客户端在编译期就会立刻报错。

第 5 步:配置 _app.tsx

createReactQueryHooks 需要某些参数通过 Context API 传入,因此要创建自定义 _app.tsx,用 withTRPC 高阶组件完成注入:

import { withTRPC } from '@trpc/next';
import { AppType } from 'next/dist/shared/lib/utils';
import type { AppRouter } from './api/trpc/[trpc]';

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

export default withTRPC<AppRouter>({
  config(config) {
    /**
     * If you want to use SSR, you need to use the server's full URL
     * @see https://trpc.io/docs/ssr
     */
    const url = process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}/api/trpc`
      : 'http://localhost:3000/api/trpc';

    return {
      url,
      /**
       * @see https://tanstack.com/query/v3/docs/react/reference/QueryClient
       */
      // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
    };
  },
  /**
   * @see https://trpc.io/docs/ssr
   */
  ssr: true,
})(MyApp);

需要注意 url 在浏览器端与 SSR 端有微妙差别:config 回调在客户端请求时会收到 ctx,而代码注释强调——若开启 SSR,服务器端执行查询时必须使用服务的完整 URLVERCEL_URL 指向部署域名),否则服务端自调用 localhost:3000 未必可达。config 回调的 ctx 参数还能让你访问到 Next.js 的 req 对象,这一点对下文要讲的 headers() 透传至关重要。

第 6 步:发起 API 请求

在任意组件中调用 hooks 即可:

import { trpc } from '../utils/trpc';

export default function IndexPage() {
  const hello = trpc.useQuery(['hello', { text: 'client' }]);
  if (!hello.data) {
    return <div>Loading...</div>;
  }
  return (
    <div>
      <p>{hello.data.greeting}</p>
    </div>
  );
}

useQuery 的第一参数是 [path, input] 元组:输入是可选时甚至可以省略第二个元素(如 trpc.useQuery(['hello'])),并同时支持像 trpc.useQuery(['post.byId', { id: 1 }]) 这样携带复合路径的调用,path 与 input 均有类型推导(详见 useQuery())。

withTRPC() 配置项详解

config 回调

config 是一个函数,返回值用于同时配置 tRPC 客户端与 React Query 客户端;它的入参 ctx 能让你访问 Next.js 的 req 对象等信息。返回对象的属性分两类:

  • 必选(二选一,恰好提供一个)
    • url:你的 API 地址;
    • links:自定义 tRPC Client 与 tRPC Server 之间的数据流(例如组合 httpBatchLinkloggerLink 等),详见 客户端 links 文档
  • 可选
    • queryClientConfig:传给内部 React Query QueryClient 的配置对象,可用于调整默认的 staleTimeretry 等策略;
    • headers:一个对象或返回对象的函数,用于为 tRPC 出站请求附加请求头;
    • transformer:应用于出站/入站 payload 的数据转换器(详见 Data Transformers);
    • fetch:自定义 tRPC 内部使用的 fetch 实现;
    • AbortController:自定义 tRPC 内部使用的 AbortController 实现。

transformer 一项尤其值得展开:默认情况下 JSON 无法表达 DateMapSet 等类型,若希望 resolver 返回这些对象并在客户端原样使用,需要在客户端与服务端同时配置同一 transformer。最常用的方案是 superjson(withTRPC 的 config 与 server router 上都要配置),同时保证传入对象满足 { serialize, deserialize } 这一 DataTransformer 接口即可(详见 data-transformers.md)。

ssr 布尔开关(默认 false

控制 tRPC 在服务端渲染页面时是否 await 所有查询,默认值为 false。当设为 true 时,tRPC 会在服务端通过 getInitialProps 预取全部查询并把数据随页面一起下发,避免客户端首屏"先 loading 再请求"的闪烁。开启 SSR 后有两件配套事项:

  • config 回调必须区分浏览器端与 SSR 端:客户端可直接使用相对路径 url: '/api/trpc',而 SSR 端需拼接服务完整 URL,并手动透传客户端请求头(尤其是 Cookie,否则 SSR 过程中无法维持登录态);若运行在 Node 18 上还要剔除 connection 头,因为它属于浏览器受限请求头,保留会导致 TRPCClientError: fetch failed
  • 由于 SSR 依赖 getInitialProps 预取,它与页面级 getServerSideProps/getStaticProps 同时使用时可能互相干扰(官方曾记录相关 issue);若必须在 getStaticProps/getServerSideProps 内预取查询,请改用 SSG Helpers,详见 服务端渲染(SSR)文档

responseMeta 回调

用于在服务端渲染时控制响应头与 HTTP 状态码,典型场景是把 tRPC 调用的错误状态上抛给页面 HTTP 响应,或为页面设置 CDN 缓存策略:

export default withTRPC<AppRouter>({
  config(config) {
    /* [...] */
  },
  ssr: true,
  responseMeta({ clientErrors, ctx }) {
    if (clientErrors.length) {
      // propagate first http error from API calls
      return {
        status: clientErrors[0].data?.httpStatus ?? 500,
      };
    }
    // cache full page for 1 day + revalidate once every second
    const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
    return {
      'Cache-Control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
    };
  },
})(MyApp);

responseMeta 收到两个参数:clientErrors(本次 SSR 过程中 tRPC 调用产生的客户端错误数组)与 ctx(Next.js 页面上下文)。上例中:只要有任意一次 API 调用失败,就把首个错误的 data.httpStatus 透传为页面 HTTP 状态码;否则返回 Cache-Control 头,其中 s-maxage=1 允许 CDN 缓存 1 秒,stale-while-revalidate 允许在重新校验期间继续为陈旧内容服务长达 1 天——这是 tRPC + Next.js 实现"整页 CDN 缓存 + 极速回源"的经典手法。

从源码看 [trpc].ts 背后的运作机制

文档建议把 router 暴露在单一路由文件 pages/api/trpc/[trpc].ts,其底层依赖的是 packages/server/src/adapters/next.ts 中的 createNextApiHandler。该适配器的核心逻辑可以概括为三步:

  1. 路径还原:从 req.query['trpc'] 中读取路径段,若为字符串则直接使用,若为数组则 join('/') 拼出完整 procedure 路径(例如访问 /api/trpc/post.byId 时还原出 post.byId);
  2. 请求分发:把还原出的 path 连同 reqres 交给 nodeHTTPRequestHandler,由其查找 router 上对应 procedure 并执行校验、中间件与 resolver;
  3. 统一异常处理:通过 internal_exceptionHandler 捕获执行过程中的错误并统一序列化为 tRPC 错误协议返回。

这也是为什么整个应用的 API 只需要一个 API Route 文件:tRPC 用"路径即过程名"的约定替代了传统 REST 中一接口一路由的繁琐映射,路由数量从 N 收敛为 1,而类型安全则由 typeof appRouter 一端的单一事实来源保证。

下一步:查询、变更与示例工程

完成上述接入后,组件的查询与变更能力由 @trpc/react 提供:

  • Queries(查询):通过 trpc.useQuery 读取数据,参考 useQuery()
  • Mutations(变更):通过 trpc.useMutation 写入数据(如登录、创建文章),其用法与 React Query 的 mutation 一致,例如 trpc.useMutation(['login']) 返回的 mutateisLoadingerror 等状态可直接驱动表单交互,参考 useMutation()

如果是在全新项目中起步,与其手动搭骨架,不如直接以官方示例工程为起点——本仓库 examples/ 目录下提供了多个可直接运行的最小参考,其中 next-minimal-starter 即包含 pages/api/trpc/[trpc].ts_app.tsxserver/utils/trpc.ts 的完整落点,可作为逐文件对照学习的模板(更完整的 v9 时代示例清单见 Example Apps)。若需深入了解 SSR 的完整配置(含 header 透传与缓存头设置)、SSG 预取模式及订阅(subscription)能力,可继续阅读同目录下的 ssr.mdssg.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395