尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

create-t3-app 中的 tRPC 实战指南:零代码生成的端到端类型安全 API

create-t3-app 中的 tRPC 实战指南:零代码生成的端到端类型安全 API create-t3-app 中的 tRPC 实战指南零代码生成的端到端类型安全 API【免费下载链接】create-t3-appThe best way to start a full-stack, typesafe Next.js app项目地址: https://gitcode.com/gh_mirrors/cr/create-t3-apptRPC 是 create-t3-app 默认栈T3 Stack中的核心一员它让开发者可以用纯 TypeScript 编写后端过程procedure并通过类型推断让前端获得完整的类型安全与自动补全全程无需代码生成、也没有运行时开销。本文以 tRPC 使用文档 为骨架结合仓库中 tRPC 安装器 与 模板源码 的实现细节完整讲解 procedure、router、context、错误推断、外部调用与常见实战片段帮助你彻底掌握这套前后端融为一体的开发范式。tRPC 是什么为什么 T3 Stack 选择它tRPC 允许你在不进行任何代码生成、不引入运行时额外开销的前提下编写端到端类型安全的 API。它依靠 TypeScript 强大的类型推断能力从你的 API router 类型定义中推导出前端调用所需的全部类型信息让前端调用后端过程时获得完整的类型安全与自动补全。使用 tRPC 时前端与后端前所未有地贴近开发者体验大幅提升。与传统的 REST 风格 API 相比tRPC 的核心差异在于你不再为每个路由手写 URL、手动校验 HTTP 方法、手写请求/响应类型而是直接调用一个 TypeScript 函数。如何使用 tRPC从 procedure 到前端调用tRPC 的使用方式是在后端编写 TypeScript 函数procedure然后在前端直接调用。一个最简单的 tRPC procedure 示例如下const userRouter createTRPCRouter({ getById: publicProcedure.input(z.string()).query(({ ctx, input }) { return ctx.prisma.user.findFirst({ where: { id: input, }, }); }), });这里的关键概念拆解如下procedure过程相当于传统后端中的一个路由处理器。上例先用.input()通过 Zod 校验输入与 环境变量指南 中使用的是同一个校验库这里确保输入必须是字符串如果输入不是字符串tRPC 会返回一条信息丰富的错误而非静默失败。resolver解析器在.input()之后链式连接可以是query、mutation或subscription三种类型之一。示例中 resolver 通过 Prisma 指南 中的客户端查询数据库返回id匹配的用户记录。在仓库的模板中create-t3-app 会生成一个现成的示例 router位于 cli/template/extras/src/server/api/routers/post/base.ts它同时展示了 query 与 mutation 两种写法export const postRouter createTRPCRouter({ hello: publicProcedure .input(z.object({ text: z.string() })) .query(({ input }) { return { greeting: Hello ${input.text}, }; }), create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ input }) { const post: Post { id: posts.length 1, name: input.name, }; posts.push(post); return post; }), getLatest: publicProcedure.query(() { return posts.at(-1) ?? null; }), });router 与 appRouter过程的组织方式procedure 定义在routers中一个 router 代表一组具有共享命名空间的相关过程。你可以为users、posts、messages各建一个 router再把它们合并进一个统一的appRouterconst appRouter createTRPCRouter({ users: userRouter, posts: postRouter, messages: messageRouter, }); export type AppRouter typeof appRouter;注意这里只需要导出 router 的类型定义这意味着前端永远不会引入任何服务端代码。在 create-t3-app 生成的项目中cli/template/extras/src/server/api/root.ts 还额外导出了createCaller用于服务端调用见下文如何从外部调用 API一节export const appRouter createTRPCRouter({ post: postRouter, }); // export type definition of API export type AppRouter typeof appRouter; export const createCaller createCallerFactory(appRouter);前端调用类型安全与自动补全tRPC 为tanstack/react-query提供了封装让你既能使用 React Query 全部 hooks 能力又能获得类型化的 API 调用。前端调用示例如下import { useRouter } from next/router; import { api } from ../../utils/api; const UserPage () { const { query } useRouter(); const userQuery api.users.getById.useQuery(query.id); return ( div h1{userQuery.data?.name}/h1 /div ); };你会立刻体会到自动补全和类型安全带来的体验写下api.后所有 router 就会出现在自动补全列表中选中某个 router 后它的 procedure 也会随之列出。如果传入的参数与后端定义的校验器不匹配TypeScript 会直接报错。错误推断让 Zod 校验错误在前端可见默认情况下create-t3-app 会配置一个 error formatter当后端发生校验错误时前端可以推断出 Zod 错误的具体字段。对应的模板实现位于 cli/template/extras/src/server/api/trpc-pages/base.ts初始化 tRPC 时传入errorFormatter用error.cause instanceof ZodError判断错误来源并把zodError挂到错误的data上const t initTRPC.contexttypeof createTRPCContext().create({ transformer: superjson, errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, });前端的使用方式function MyComponent() { const { mutate, error } api.post.create.useMutation(); return ( form onSubmit{(e) { e.preventDefault(); const formData new FormData(e.currentTarget); mutate({ title: formData.get(title) }); }} input nametitle / {error?.data?.zodError?.fieldErrors.title ( {/** mutate returned with an error on the title */} span classNamemb-8 text-red-500 {error.data.zodError.fieldErrors.title} /span )} ... /form ); }create-t3-app 生成的 tRPC 文件全解tRPC 需要相当多的样板代码create-t3-app 会替你全部搭建好。下面逐一剖析生成的文件。安装器 cli/src/installers/trpc.ts 会依据选项安装依赖tanstack/react-query、superjson、trpc/server、trpc/client、trpc/react-queryApp Router 下额外加server-onlyPages Router 下加trpc/next并按 App Router / Pages Router、是否启用鉴权NextAuth / better-auth、是否启用数据库Prisma / Drizzle 的组合从 cli/template/extras 中选择对应的模板文件复制到项目。pages/api/trpc/[trpc].tsPages Router 入口这是 API 的入口负责暴露 tRPC router。模板实现位于 cli/template/extras/src/pages/api/trpc/[trpc].tsimport { createNextApiHandler } from trpc/server/adapters/next; import { env } from ~/env; import { appRouter } from ~/server/api/root; import { createTRPCContext } from ~/server/api/trpc; // export API handler export default createNextApiHandler({ router: appRouter, createContext: createTRPCContext, onError: env.NODE_ENV development ? ({ path, error }) { console.error( ❌ tRPC failed on ${path ?? no-path}: ${error.message} ); } : undefined, });正常情况下你很少改动这个文件但了解其原理很有用导出的createNextApiHandler本质上是一个 Next.js API handler接收 request 和 response 对象。这意味着你可以用任何中间件包装它例如 启用 CORS。如果你选用 App Routercreate-t3-app 生成的则是 cli/template/extras/src/app/api/trpc/[trpc]/route.ts它改用fetchRequestHandler并同时导出GET与POSTimport { fetchRequestHandler } from trpc/server/adapters/fetch; import { type NextRequest } from next/server; import { env } from ~/env; import { appRouter } from ~/server/api/root; import { createTRPCContext } from ~/server/api/trpc; const createContext async (req: NextRequest) { return createTRPCContext({ headers: req.headers, }); }; const handler (req: NextRequest) fetchRequestHandler({ endpoint: /api/trpc, req, router: appRouter, createContext: () createContext(req), onError: env.NODE_ENV development ? ({ path, error }) { console.error( ❌ tRPC failed on ${path ?? no-path}: ${error.message} ); } : undefined, }); export { handler as GET, handler as POST };server/api/trpc.ts这个文件分为两个部分context 创建与 tRPC 初始化。1. context 定义context 是所有 tRPC procedure 都能访问的数据非常适合存放数据库连接、鉴权信息等。create-t3-app 使用两个函数以便在拿不到 request 对象时仍能使用 context 的子集createInnerTRPCContext定义不依赖 request 的 context例如数据库连接。可用于集成测试或 ssg-helpers 这类没有 request 对象的场景。createTRPCContext定义依赖 request 的 context例如用户 session。通过opts.req获取 session再传给createInnerTRPCContext组装出最终 context。在 Pages Router 的模板cli/template/extras/src/server/api/trpc-pages/base.ts中createTRPCContext接收CreateNextContextOptions而在 App Router 模板cli/template/extras/src/server/api/trpc-app/base.ts中context 接收{ headers: Headers }因为请求头是 App Router 下唯一可稳定获取的请求信息。启用 NextAuth 时模板会在 context 中注入session启用 Prisma/Drizzle 时则会注入数据库客户端db。2. tRPC 初始化初始化 tRPC 并定义可复用的 procedure 与 middleware。按约定你不应导出整个t对象而是创建可复用的 procedure 和 middleware 并导出它们。模板正是如此导出createTRPCRouter、createCallerFactory和publicProcedure。模板还内置了一个timingMiddleware它会给每个 procedure 计时并在开发环境注入 100~500ms 的随机人工延迟用来模拟生产环境才会出现的网络延迟、帮助发现意外的瀑布式请求waterfall。publicProcedure即t.procedure.use(timingMiddleware)。数据转换器 superjson你会注意到模板使用superjson作为 data transformer。这保证数据类型在到达客户端时被保留——例如你发送一个Date对象客户端收到的是Date而不是大多数 API 返回的字符串。server/api/routers/*.ts这里是定义 API 路由与过程的地方。按约定为相关过程创建独立的 router 中createmutation 直接写入数据库create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { name: input.name, }, }); }), getLatest: publicProcedure.query(async ({ ctx }) { const post await ctx.db.post.findFirst({ orderBy: { createdAt: desc }, }); return post ?? null; }),server/api/root.ts在这里合并routers/**中定义的所有子 router 为单一 app router并导出AppRouter类型与createCaller。utils/api.tsPages Router 前端入口这是 tRPC 的前端入口模板位于 cli/template/extras/src/utils/api.ts。它导入 router 的类型定义创建 tRPC client 与 react-query hooks由于后端启用了superjson转换器前端也必须启用它后端序列化的数据需要在前端反序列化。你在这里定义 tRPC links它决定客户端到服务端的请求流程。模板使用默认的httpBatchLink以支持请求批处理以及loggerLink在开发期间输出有用的请求日志仅当NODE_ENV development或收到错误响应时启用。getBaseUrl的取值逻辑值得注意浏览器端用相对路径SSR 时若存在VERCEL_URL环境变量则使用该 Vercel 域名否则回退到http://localhost:${process.env.PORT ?? 3000}。最后文件导出两个推断辅助类型RouterInputs与RouterOutputs供前端推断输入/输出类型export type RouterInputs inferRouterInputsAppRouter; export type RouterOutputs inferRouterOutputsAppRouter;如果使用 App Router前端入口变为src/trpc/react.tsx模板与src/trpc/server.ts模板react.tsx导出api createTRPCReactAppRouter()和TRPCReactProvider组件后者同时挂载QueryClientProvider与api.Providerlink 使用httpBatchStreamLink并设置x-trpc-source: nextjs-react请求头QueryClient 在浏览器端采用单例模式、服务端每次新建。server.ts供 React Server Component 使用通过createHydrationHelpers导出api与HydrateClientcontext 从next/headers中读取请求头并标记x-trpc-source: rsc。query-client.ts 配置 React Query 的staleTime30 秒避免 SSR 后客户端立即重复请求并使用 superjson 序列化/反序列化脱水数据。如何从外部调用我的 API普通 API 可以用curl、Postman、fetch或浏览器直接调用端点。tRPC 略有不同如果不想通过 tRPC client 调用 procedure官方推荐两种方式。方式一对外暴露单个 procedure如果你只想暴露单个 procedure请使用服务端调用server side calls。它可以创建一个普通的 Next.js API 端点但复用 tRPC procedure 的 resolver 部分。因为root.ts导出了createCaller实现非常直接import { type NextApiRequest, type NextApiResponse } from next; import { appRouter, createCaller } from ../../../server/api/root; import { createTRPCContext } from ../../../server/api/trpc; const userByIdHandler async (req: NextApiRequest, res: NextApiResponse) { // Create context and caller const ctx await createTRPCContext({ req, res }); const caller createCaller(ctx); try { const { id } req.query; const user await caller.user.getById(id); res.status(200).json(user); } catch (cause) { if (cause instanceof TRPCError) { // An error from tRPC occurred const httpCode getHTTPStatusCodeFromError(cause); return res.status(httpCode).json(cause); } // Another error occurred console.error(cause); res.status(500).json({ message: Internal server error }); } }; export default userByIdHandler;方式二把每个 procedure 都暴露为 REST 端点如果想暴露所有 procedure可以了解社区插件trpc-openapi为 procedure 提供少量额外元数据即可从 tRPC router 生成符合 OpenAPI 规范的 REST API。补充说明它本质上就是 HTTP 请求tRPC 通过 HTTP 通信因此理论上也可以用普通 HTTP 请求调用 procedure。但由于 tRPC 使用 RPC 协议语法会比较繁琐。你可以在浏览器 Network 面板观察 tRPC 请求与响应的真实格式但建议仅作为学习用途正式场景仍优先使用上述两种方案之一。与 Next.js API 端点的对比假设我们要从数据库取一个 user 对象返回给前端先看传统 Next.js API 端点写法import { type NextApiRequest, type NextApiResponse } from next; import { prisma } from ../../../server/db; const userByIdHandler async (req: NextApiRequest, res: NextApiResponse) { if (req.method ! GET) { return res.status(405).end(); } const { id } req.query; if (!id || typeof id ! string) { return res.status(400).json({ error: Invalid id }); } const examples await prisma.example.findFirst({ where: { id, }, }); res.status(200).json(examples); }; export default userByIdHandler;import { useState, useEffect } from react; import { useRouter } from next/router; const UserPage () { const router useRouter(); const { id } router.query; const [user, setUser] useState(null); useEffect(() { fetch(/api/user/${id}) .then((res) res.json()) .then((data) setUser(data)); }, [id]); };对比前文的 tRPC 示例可以清楚看到 tRPC 的优势不必为每条路由指定 URL移动代码时 URL 容易成为排查难题整个 router 就是一个带自动补全的对象不需要手动校验使用了哪个 HTTP 方法不需要在 procedure 中手动校验 query 或 body 的数据Zod 已经处理不必构造响应对象可以直接抛错、直接返回值或对象就像写普通 TypeScript 函数前端调用 procedure 自带自动补全与类型安全。实用代码片段启用 CORS当 API 需要被其他域名消费时例如包含 React Native 应用的 monorepo你可能需要启用 CORS。利用createNextApiHandler是可包装的 Next.js handler 这一特性用nextjs-cors中间件包一层即可import { type NextApiRequest, type NextApiResponse } from next; import { createNextApiHandler } from trpc/server/adapters/next; import { appRouter } from ~/server/api/root; import { createTRPCContext } from ~/server/api/trpc; import cors from nextjs-cors; const handler async (req: NextApiRequest, res: NextApiResponse) { // Enable cors await cors(req, res); // Create and call the tRPC handler return createNextApiHandler({ router: appRouter, createContext: createTRPCContext, })(req, res); }; export default handler;乐观更新Optimistic updates乐观更新指在 API 调用完成前先更新 UI让用户不必等待请求返回就能看到操作结果体验更好。不过对数据准确性要求很高的应用应谨慎使用乐观更新因为它并非后端状态的真实呈现。React Query 官方文档有更详细的说明。核心实现如下const MyComponent () { const listPostQuery api.post.list.useQuery(); const utils api.useUtils(); const postCreate api.post.create.useMutation({ async onMutate(newPost) { // Cancel outgoing fetches (so they dont overwrite our optimistic update) await utils.post.list.cancel(); // Get the data from the queryCache const prevData utils.post.list.getData(); // Optimistically update the data with our new post utils.post.list.setData(undefined, (old) [...old, newPost]); // Return the previous data so we can revert if something goes wrong return { prevData }; }, onError(err, newPost, ctx) { // If the mutation fails, use the context-value from onMutate utils.post.list.setData(undefined, ctx.prevData); }, onSettled() { // Sync with server once mutation has settled utils.post.list.invalidate(); }, }); };示例集成测试下面是一个使用 Vitest 的集成测试示例用于验证 router 工作正常、输入解析器推断出正确类型、返回数据符合预期。它复用了createInnerTRPCContext——这正是前面提到的无需 request 对象即可构造 context的典型场景import { type inferProcedureInput } from trpc/server; import { expect, test } from vitest; import { appRouter, type AppRouter } from ~/server/api/root; import { createInnerTRPCContext } from ~/server/api/trpc; test(example router, async () { const ctx await createInnerTRPCContext({ session: null }); const caller appRouter.createCaller(ctx); type Input inferProcedureInputAppRouter[example][hello]; const input: Input { text: test, }; const example await caller.example.hello(input); expect(example).toMatchObject({ greeting: Hello test }); });如果 procedure 受保护需要鉴权可以在创建 context 时传入一个模拟的session对象test(protected example router, async () { const ctx await createInnerTRPCContext({ session: { user: { id: 123, name: John Doe }, expires: 1, }, }); const caller appRouter.createCaller(ctx); // ... });进一步阅读环境变量指南tRPC 与 Zod 校验同源的用法文中错误推断也依赖 ZodPrisma 指南procedure 中如何通过 context 访问数据库NextAuth 指南如何在 context 中注入 session 并保护 procedure仓库源码tRPC 安装器、Pages Router 模板入口、App Router 模板入口、trpc-pages 模板、trpc-app 模板、root.ts 模板【免费下载链接】create-t3-appThe best way to start a full-stack, typesafe Next.js app项目地址: https://gitcode.com/gh_mirrors/cr/create-t3-app创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表