ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

在 Express.js 中接入 tRPC:使用官方适配器构建类型安全的端到端 API(v9.x 实战指南)

在 Express.js 中接入 tRPC:使用官方适配器构建类型安全的端到端 API(v9.x 实战指南) 在 Express.js 中接入 tRPC使用官方适配器构建类型安全的端到端 APIv9.x 实战指南【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本指南基于 tRPC 官方 v9.x 文档《Usage with Express.js》编写讲解如何把 tRPC 无缝接入既有 Express.js 项目从安装依赖、定义类型安全的路由到通过内置 Express 适配器将整个 Router 挂载为中间件最终获得可被 HTTP 直接调用的 API 端点。读完本文你将掌握createExpressMiddleware的完整接入流程、Context 的注入方式并能参照仓库内的完整示例工程直接上手。背景tRPC 如何与 Express.js 结合tRPC 的核心价值在于端到端类型安全在服务端用 TypeScript 定义 Router 后客户端可以在不编写任何接口文档、不做手工类型声明的情况下获得与后端完全一致的自动补全与编译期类型检查。Express.js 是 Node.js 生态中最常用的 HTTP 框架之一因此 tRPC 官方内置了针对 Express 的适配器把 Router 包装成标准的 Express 中间件使其可以与其他 Express 路由、中间件生态共存。仓库中与本主题强相关的配套资源包括完整可运行的示例工程examples/express-server展示带鉴权 Context、订阅EventEmitter与批量调用的完整用法更精简的入门版见 examples/express-minimal适配器源码packages/server/src/adapters/express.ts适配器技能说明packages/server/skills/adapter-express/SKILL.md。版本说明本文正文严格遵循 v9.x 文档所对应的trpc/serverv9 APItrpc.router()链式调用风格文末会给出当前仓库v10/v11 时代使用initTRPC的写法差异提示。本文中引用的源码均以当前仓库实际内容为准。1. 安装依赖在已有 Express.js 项目的根目录中执行yarn add trpc/server zod同样可以使用npm install trpc/server zod或pnpm add trpc/server zod完成安装。其中Zod 并不是必需的依赖它只是下面示例 Router 中用来做输入校验的库。tRPC 的校验层是可替换的若你的项目已经使用其他校验方案或不需要输入校验可以不安装 Zod。2. 创建 tRPC Router首先定义 API 的类型与逻辑。一个典型的 Router 同时包含查询query与变更mutation两类 Procedure分别对应 HTTP 语义中的读与写import * as trpc from trpc/server; import { z } from zod; const appRouter trpc .router() .query(getUser, { input: z.string(), async resolve(req) { req.input; // string return { id: req.input, name: Bilbo }; }, }) .mutation(createUser, { // 用 Zod 校验输入 input: z.object({ name: z.string().min(5) }), async resolve(req) { // 这里可以接入任意 ORM return await UserModel.create({ data: req.input, }); }, }); // 导出 API 的类型定义客户端将基于它获得类型推断 export type AppRouter typeof appRouter;这段代码的关键点.query(getUser, ...)定义只读操作输入为z.string()其resolve回调内req.input已被推断为string类型.mutation(createUser, ...)定义写入操作输入经z.object({ name: z.string().min(5) })校验长度不足 5 的请求会在进入resolve前被自动拒绝export type AppRouter typeof appRouter是 tRPC 类型安全链路的源头——客户端import type { AppRouter } from ./server后即可获得完整类型。关于路由文件膨胀当 Router 文件过大时应当把路由拆分为多个子 Router每个子 Router 放在独立文件中最后通过合并merge组合成一个根appRouter。合并方式详见 v9 文档 merging-routers.md 以及 v9 Context 说明 context.md。仓库示例 examples/express-server/src/server.ts 演示了更完整的拆分子路由postRouter、messageRouter、内联的admin路由并合并进根 Router 的写法。3. 使用 Express.js 适配器挂载路由tRPC 开箱即用地包含了 Express 适配器trpc/server/adapters/express它能把 Router 转换成一个标准的 Express 中间件import * as trpcExpress from trpc/server/adapters/express; const appRouter /* ... */; const app express(); // 该函数会在每次请求时被调用用于构造 Context const createContext ({ req, res, }: trpcExpress.CreateExpressContextOptions) ({}) // no context type Context trpc.inferAsyncReturnTypetypeof createContext; app.use( /trpc, trpcExpress.createExpressMiddleware({ router: appRouter, createContext, }) ); app.listen(4000);接入过程只需要三步理解下面三个核心概念即可createExpressMiddleware将 Router 与 Context 工厂包装成 Express 中间件挂载在路径前缀/trpc下。参数对象中的router即上一步定义的appRouter。createContext与CreateExpressContextOptions该工厂函数在每个请求到来时执行一次接收 Express 的req与res对象。你可以在其中解析req.headers.authorization、读取 session、创建数据库连接等返回值会成为所有 Procedure 的ctx。上例返回空对象即无 Context。trpc.inferAsyncReturnTypetypeof createContext把createContext的异步返回类型推导为Context类型供后续给 Router 标注 Context 使用v9 中配合trpc.routerContext()显式声明。在 examples/express-server/src/server.ts 中可以看一个带鉴权的createContext实战写法它读取req.headers.authorization当值不为secret时返回user: null否则返回用户对象随后admin.secret这类受保护 Procedure 依赖该ctx.user抛出TRPCErrorUNAUTHORIZED/FORBIDDEN。端点如何映射到 HTTP挂载完成后你的 Procedure 立刻可以通过 HTTP 访问端点HTTP URIgetUserGET http://localhost:4000/trpc/getUser?inputINPUT其中INPUT是 URI 编码后的 JSON 字符串createUserPOST http://localhost:4000/trpc/createUserreq.body形如{name: string}且需满足min(5)校验即Procedure 的完整路径 挂载前缀/trpc Procedure 名称。GET请求通过?input传递 URI 编码的 JSON 输入POST请求则把输入放在请求体中。tRPC 的 HTTP 层会自动完成序列化/反序列化与 Zod 校验非法输入会返回结构化的错误响应。4. 适配器源码原理中间件背后发生了什么以当前仓库的适配器实现为参照packages/server/src/adapters/express.ts 展示了这条链路的本质export function createExpressMiddlewareTRouter extends AnyRouter( opts: NodeHTTPHandlerOptionsTRouter, express.Request, express.Response, ): express.Handler { return (req, res) { let path ; run(async () { // 从完整请求路径中截取 Procedure 名 path req.path.slice(req.path.lastIndexOf(/) 1); await nodeHTTPRequestHandler({ ...opts, req, res, path }); }).catch(internal_exceptionHandler({ req, res, path, ...opts })); }; }从中可以看出三点实现事实createExpressMiddleware的返回值就是一个express.Handler(req, res) void因此可以直接传给app.use也可以作为子中间件与其他 Express 路由自由组合中间件内部从req.path的最后一个/之后截取路径段作为 Procedure 名这就是为什么挂载前缀与 Procedure 名必须拼成/prefix/procedureName的形式真正的请求处理委托给 node-http 模块中的nodeHTTPRequestHandler这意味着 Express 适配器与独立服务器standalone、其它 Node HTTP 适配器共享同一套 HTTP 处理核心出错时通过internal_exceptionHandler统一处理异常并写出错误响应。5. 完整可运行示例与客户端调用精简版express-minimalexamples/express-minimal 是最小可运行示例路由定义 src/router.ts 使用initTRPC风格t.router({...})定义了一个带可选name入参的hello.greeting查询服务端 src/server.ts 额外保留了一个常规 Express 路由GET /用于健康检查/测试等待再把 tRPC 中间件挂载到/trpc最后app.listen(3000)。这展示了一种常见形态tRPC 中间件与既有 Express 路由并存两者互不干扰。完整版express-serverexamples/express-server 则覆盖了更多真实场景包括订阅式消息推送基于 NodeEventEmitter、分层子路由、受鉴权保护的admin.secret等。其客户端 src/client.ts 演示了 tRPC 客户端的典型用法import { createTRPCClient, httpBatchLink, loggerLink } from trpc/client; import type { AppRouter } from ./server; const trpc createTRPCClientAppRouter({ links: [ loggerLink(), httpBatchLink({ url: http://localhost:2021/trpc }), ], });注意AppRouter是从服务端import type而来的——这正是类型安全的端到端的落点客户端不用手写任何接口签名。此外该示例还展示了批处理httpBatchLink可以把多个查询合并为单个 HTTP 请求示例中Promise.all并发调用两次hello.query按请求注入 HeaderhttpBatchLink的headers选项返回{ authorization: secret }与服务端createContext的鉴权逻辑一一对应从而解锁admin.secret查询未携带该 Header 时则被服务端以TRPCError拒绝。6. 与既有 Express 路由共存时的最佳实践基于 packages/server/skills/adapter-express/SKILL.md 与仓库示例接入时建议注意以下几点6.1 常规 REST 路由与 tRPC 共存无需把整个应用都迁移到 tRPC。健康检查、静态资源、回调接口等仍可用原生 Express 路由再把/trpc前缀交给 tRPC 适配器即可。示例如下同时可叠加cors()等 Express 生态中间件import * as trpcExpress from trpc/server/adapters/express; import cors from cors; import express from express; import { createContext } from ./context; import { appRouter } from ./router; const app express(); app.use(cors()); app.get(/health, (_req, res) { res.json({ status: ok }); }); app.use( /trpc, trpcExpress.createExpressMiddleware({ router: appRouter, createContext, }), ); app.listen(4000);6.2 不要在 tRPC 之前全局注册express.json()高优先级提醒如果先全局执行app.use(express.json())再挂载 tRPC 中间件全局 body 解析器会先消费并解析请求体导致 tRPC 收到的请求体已被改写从而破坏multipart/form-dataFormData与二进制内容类型的处理。推荐做法是只把 JSON body 解析器限定在非 tRPC 路由上const app express(); // 仅对 /api 下的非 tRPC 路由启用 body 解析 app.use(/api, express.json()); app.use( /trpc, trpcExpress.createExpressMiddleware({ router: appRouter, createContext }), );这也解释了为何示例工程 examples/express-server/src/server.ts 中没有对/trpc启用全局express.json()。6.3 控制批处理上限maxBatchSize若你的客户端启用了批处理可在服务端中间件选项中设置maxBatchSize防止单个请求携带过多操作app.use( /trpc, trpcExpress.createExpressMiddleware({ router: appRouter, createContext, maxBatchSize: 10, }), );超过maxBatchSize的批处理请求会被以400 Bad Request拒绝。客户端侧应同步把httpBatchLink的maxItems设为相同数值避免请求超限。7. 版本演进提示v9 与当前 v10/v11 写法的差异v9.x 文档中的 Router 使用trpc.router().query(...)链式风格。在 v10/v11即本仓库当前主线的 packages/server 与现行文档 www/docs/server/adapters/express.md中写法统一改为先initTRPC初始化实例再通过t.router/t.procedure定义例如import { initTRPC } from trpc/server; import * as trpcExpress from trpc/server/adapters/express; const t initTRPC.contextContext().create(); const appRouter t.router({ greet: t.procedure .input(z.object({ name: z.string() })) .query(({ input }) ({ greeting: Hello, ${input.name}! })), });但适配器的接入形态没有本质变化createExpressMiddleware、createContext、CreateExpressContextOptions、app.use(/trpc, ...)的核心用法与本文一致express-minimal 与 express-server 两个示例即运行在当前写法之上。从 v10 迁移到 v11 的完整细节可参考 migrate-from-v10-to-v11.mdx。总结在 Express.js 项目中接入 tRPC 只需要三步安装依赖 → 定义可拆分的Router → 用createExpressMiddleware将其挂载到/trpc前缀。整个过程不需要额外启动独立服务所有 Procedure 立即成为可被 HTTP 访问的端点同时客户端借由AppRouter类型获得零成本的端到端类型安全。若想直接运行体验参考 examples/express-minimal 与 examples/express-server 两个示例工程的package.json脚本即可快速启动。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表