从 REST 到 tRPC:类型安全的 API 调用为什么越来越受欢迎

本文从工程实践角度分析从 REST 到 tRPC 的转变,解释类型安全 API 调用为什么越来越受欢迎。通过对比 REST 与 tRPC 在类型安全、缓存、版本控制等维度的差异,讨论适用场景、常见误区和落地建议,为 TypeScript 全栈团队提供选型参考。

前后端联调大概是整个开发流程里最消耗耐心的事情之一。前端说后端返回的字段和文档不一致,后端说前端传的参数类型不对。这种问题在项目早期还能靠沟通解决,一旦接口多起来,就会变成一场持续的拉锯战。于是有人开始思考:既然前后端都用 TypeScript,为什么不把类型直接共享给两端,让编译器帮我们盯着?

AI technology illustration

这就是 tRPC 出现的背景。tRPC 不是一个库,而是一套基于 TypeScript 的类型安全 API 调用方案。它试图把 REST 时代的“接口文档 + 手工调用”模式,变成“类型即契约”的全栈体验。

这篇文章不会急着给出“REST 已死”的结论,而是想聊聊从 REST 到 tRPC 这个转变,到底解决了什么问题,又引入了哪些新的权衡。

为什么 REST 在类型安全上天然吃亏

REST 本身是资源导向的架构风格,它并不关心数据类型。接口的请求参数和响应体都是通过 JSON 传输的,而 JSON 是一种弱类型格式。前端拿到数据后,需要自己定义 interface,或者依赖手写的类型声明。后端改了一个字段名,前端不一定能立刻发现,往往要等联调甚至线上报错才能察觉。

真正麻烦的地方在于,接口文档和代码是分离的。就算用了 Swagger 自动生成,文档里的类型和实际代码也可能不同步。而且,随着微服务拆分,前端可能要维护多个服务的接口类型,维护成本成倍增加。

想象一个中大型项目,前端团队十几个人,后端也有十几人。接口数量几百个,每次接口变更都要在群里通知,然后前端手动更新类型定义。如果你经历过这种日子,应该能理解“类型安全”这三个字的重量。

tRPC 做了什么:类型即契约

tRPC 的核心思路很简单:把后端路由定义为一个类型化的对象,前端通过这个类型安全的客户端直接调用。不需要代码生成,不需要额外的 schema 文件,一切都源自 TypeScript 的类型推断能力。

// server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      return user;
    }),
});

export type AppRouter = typeof appRouter;
// client.ts
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from './server';

const client = createTRPCClient<AppRouter>({ url: 'http://localhost:3000/trpc' });

const user = await client.getUser.query({ id: '1' });
// user 类型自动推断为 { id: string; name: string; ... } | null

这里有几个关键点:服务端用 zod 定义输入校验,输出类型由 query 函数的返回值推断。客户端通过 AppRouter 类型获得完整的类型信息。如果后端返回的结构变了,前端调用处会直接编译报错。

这背后没有魔法,本质上是 TypeScript 的类型系统和 HTTP 封装在配合工作。tRPC 只是把这两个原本分离的东西粘在了一起。

tRPC 的优势不只是类型安全

除了编译期的类型检查,tRPC 还有几个工程上的优势:

  • 不用维护独立的接口文档:类型本身成为接口契约,代码即文档。
  • 前后端并行开发更容易:只要定义好 router 的类型,前端就可以基于类型 mock 或直接开发。
  • 减少运行时错误:很多字段名、嵌套结构的问题在编译期就被发现,而不是等线上报警。

但也要清醒地看到,tRPC 并不是万能的。它最适用的场景是前后端都使用 TypeScript 的全栈项目,比如 Next.js、Remix、Express 等。如果客户端是第三方应用,或者需要开放 API 给其他语言,REST 或 GraphQL 可能更合适。

REST 与 tRPC 的关键对比

维度 REST tRPC
类型安全 依赖 Swagger 或手写类型 原生类型推断
接口文档 OpenAPI 等外部工具 类型即文档
学习成本 低,通用标准 中等,需理解 TS 类型系统
调试工具 Postman、浏览器 DevTools 需要 tRPC 插件或自定义日志
HTTP 缓存 天然支持 默认不支持,需额外设计
版本控制 可以独立版本化 类型变更影响所有客户端
适用场景 公开 API、多语言客户端 TypeScript 全栈内部服务

使用 tRPC 前需要认清的几个误区

误区一:以为 tRPC 不需要写任何校验。实际上,tRPC 依赖输入解析(比如 zod)来保证运行时安全。如果不用 zod,类型只是编译期的幻觉,运行时可能收到脏数据。

误区二:认为 tRPC 是 GraphQL 的替代品。虽然两者都追求高效的前后端协作,但 GraphQL 强调客户端按需查询,tRPC 则是按函数调用。它们解决的问题有重叠,但设计哲学不同。

误区三:把 tRPC 当作 REST 的完全替代品,强行改造所有接口。tRPC 不适合公开 API,也不适合需要复杂 HTTP 缓存、代理和网关策略的场景。内部服务之间用 tRPC 很爽,但对外还是要用标准协议。

提醒一下:tRPC 的类型安全是“编译期”的,不是“运行期”的。输入校验、错误处理、日志这些工程基本功,一个都不能少。

在现有项目里落地 tRPC 的建议

如果你决定在项目中尝试 tRPC,建议从新模块开始,而不是重构现有 REST 接口。可以把 tRPC 路由挂在现有服务上,比如 /trpc 路径,逐步迁移。同时注意以下几点:

  1. 类型定义放在共享包中,或者直接导入服务端类型(但要避免客户端依赖服务端代码,可以通过类型导出解决)。
  2. 使用 zod 做输入校验,尽量覆盖边界情况。
  3. 日志和错误处理要完善,因为 tRPC 的错误信息默认比较简略。
  4. 考虑查询的缓存策略,tRPC 默认没有 React Query 那样的缓存机制,需要自己接入。

一个小型全栈团队,用 Next.js 写前端,后端用 Node.js,两个人负责所有开发。他们引入 tRPC 后,发现接口联调的时间几乎降为零,因为类型不匹配在编译期就暴露了。但当他们想给第三方公司开放 API 时,发现 tRPC 的客户端是 TypeScript 专用的,对方只能手写 HTTP 调用,这时他们才意识到 tRPC 的边界。

另一个场景:大型团队,几百个接口,前期为了快速交付,没有引入类型安全,后来维护成本爆炸,开始尝试用 tRPC 重写核心链路。这个过程中发现,老接口的语义混乱、嵌套太深,迁移成本不低。所以 tRPC 不是万能药,它解决的是“新项目”或“可控项目”里的类型断裂问题,而不是历史包袱。

总结

从 REST 到 tRPC,本质上是把编程语言层面的类型系统延伸到网络边界。它让前后端协作从“约定”变成了“强制”。但技术选型永远没有银弹,tRPC 用起来舒服,前提是你能接受它的限制:纯 TypeScript 生态、内部服务优先、需要更主动的缓存设计。如果你的团队恰好是 TypeScript 全栈,并且被接口类型问题折磨过,那么 tRPC 值得一试。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/475/

(0)
上一篇 26分钟前
下一篇 10分钟前

相关推荐