在 Next.js 中接入 tRPC:Pages Router 下端到端类型安全的完整集成指南(trpc v9)
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 请求(hello、post.list、post.create 等)都路由到同一个处理函数,由适配器根据 URL 中的路径段还原出 procedure 的完整路径。这一点在 packages/server/src/adapters/next.ts 的 createNextApiHandler 实现中体现得很直白:它从 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 的一层薄封装,useQuery、useMutation等 hooks 的缓存、重试、失效机制全部由 React Query 提供,因此 v9 时代要求安装react-query@3(React Query v3)。 - Zod 并非强制:绝大多数示例使用 Zod 做输入校验,tRPC 官方也强烈推荐,但你可以自由选择 Yup、Superstruct、io-ts 等任意校验库。事实上,任何包含
parse、create或validateSync方法的对象都能作为输入校验器使用。
第 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.create、user.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,服务器端执行查询时必须使用服务的完整 URL(VERCEL_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 之间的数据流(例如组合httpBatchLink、loggerLink等),详见 客户端 links 文档。
- 可选:
queryClientConfig:传给内部 React QueryQueryClient的配置对象,可用于调整默认的staleTime、retry等策略;headers:一个对象或返回对象的函数,用于为 tRPC 出站请求附加请求头;transformer:应用于出站/入站 payload 的数据转换器(详见 Data Transformers);fetch:自定义 tRPC 内部使用的fetch实现;AbortController:自定义 tRPC 内部使用的AbortController实现。
transformer 一项尤其值得展开:默认情况下 JSON 无法表达 Date、Map、Set 等类型,若希望 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。该适配器的核心逻辑可以概括为三步:
- 路径还原:从
req.query['trpc']中读取路径段,若为字符串则直接使用,若为数组则join('/')拼出完整 procedure 路径(例如访问/api/trpc/post.byId时还原出post.byId); - 请求分发:把还原出的
path连同req、res交给nodeHTTPRequestHandler,由其查找 router 上对应 procedure 并执行校验、中间件与 resolver; - 统一异常处理:通过
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'])返回的mutate、isLoading、error等状态可直接驱动表单交互,参考 useMutation()。
如果是在全新项目中起步,与其手动搭骨架,不如直接以官方示例工程为起点——本仓库 examples/ 目录下提供了多个可直接运行的最小参考,其中 next-minimal-starter 即包含 pages/api/trpc/[trpc].ts、_app.tsx、server/ 与 utils/trpc.ts 的完整落点,可作为逐文件对照学习的模板(更完整的 v9 时代示例清单见 Example Apps)。若需深入了解 SSR 的完整配置(含 header 透传与缓存头设置)、SSG 预取模式及订阅(subscription)能力,可继续阅读同目录下的 ssr.md 与 ssg.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00