
2026年REST API vs GraphQL vs tRPC:你应该选择哪个?
📷 Stanislav Kondratiev / Pexels2026年REST API vs GraphQL vs tRPC:你应该选择哪个?
针对现代Web开发的REST、GraphQL和tRPC的详细比较。了解每种方案的优缺点、性能特征和最佳使用场景。
选择前端与后端的通信方式是Web项目中最重要的架构决策之一。2026年,开发者拥有三个成熟且经过实战验证的选项:REST、GraphQL和tRPC。每种方案都有各自独特的优势、权衡和理想的使用场景。本文提供全面的比较,帮助你为下一个项目做出正确的选择。
REST API:确立的标准
REST(Representational State Transfer)作为主流API范式已超过二十年。它将HTTP方法映射到资源上的CRUD操作,使其直观且几乎所有开发者都能理解。
REST的工作原理
REST API将资源暴露为URL。客户端使用标准HTTP方法与其交互:
// GET a list of users
const response = await fetch('https://api.example.com/users');
const users = await response.json();
// GET a single user
const user = await fetch('https://api.example.com/users/42');
// POST to create a new user
const newUser = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Alice Johnson',
email: 'alice@example.com',
}),
});
// PUT to update a user
await fetch('https://api.example.com/users/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice Smith' }),
});
// DELETE a user
await fetch('https://api.example.com/users/42', {
method: 'DELETE',
});
服务端REST示例(Express)
import express from 'express';
const app = express();
app.use(express.json());
// GET /users
app.get('/users', async (req, res) => {
const { page = 1, limit = 20, sort = 'name' } = req.query;
const users = await db.users.findMany({
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
orderBy: { [sort as string]: 'asc' },
});
res.json({ data: users, page: Number(page), limit: Number(limit) });
});
// GET /users/:id
app.get('/users/:id', async (req, res) => {
const user = await db.users.findUnique({
where: { id: Number(req.params.id) },
include: { posts: true, profile: true },
});
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
// POST /users
app.post('/users', async (req, res) => {
const user = await db.users.create({ data: req.body });
res.status(201).json(user);
});
REST的优势
- 普遍理解:每个开发者、每种语言、每个平台都支持REST。
- HTTP缓存:GET请求可以被浏览器、CDN和代理开箱即用地缓存。
- 无状态且简单:每个请求包含服务器所需的所有信息。
- 成熟的工具:Swagger/OpenAPI、Postman及无数库。
- 状态码有意义:200、201、404、422、500都传达了具体的信息。
REST的劣势
- 过度获取:端点返回所有字段,即使客户端只需要其中几个。
- 不足获取:获取相关数据通常需要多次往返。
- 无内置类型安全:没有OpenAPI代码生成等额外工具,客户端和服务器之间没有契约。
- 版本控制挑战:破坏性更改通常需要
/v2/端点或基于头部的版本控制。
GraphQL:灵活的查询
GraphQL由Facebook创建,2015年开源,允许客户端在单个请求中精确指定所需的数据。它使用类型化的Schema来定义API表面。
GraphQL的工作原理
// A GraphQL query
const query = `
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts(first: 5) {
title
createdAt
}
followers {
totalCount
}
}
}
`;
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables: { id: '42' },
}),
});
const { data } = await response.json();
// data.user.name, data.user.posts, data.user.followers.totalCount
服务端GraphQL Schema
// schema.ts (using a schema-first approach)
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts(first: Int, after: String): PostConnection!
followers: FollowerCount!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
postCreated: Post!
}
`;
Resolver
const resolvers = {
Query: {
user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
return ctx.db.users.findUnique({ where: { id } });
},
users: async (_: unknown, args: PaginationArgs, ctx: Context) => {
return paginateUsers(ctx.db, args);
},
},
User: {
posts: async (parent: User, args: PaginationArgs, ctx: Context) => {
return ctx.loaders.userPosts.load({ userId: parent.id, ...args });
},
followers: async (parent: User, _: unknown, ctx: Context) => {
const count = await ctx.db.followers.count({
where: { followeeId: parent.id },
});
return { totalCount: count };
},
},
Mutation: {
createUser: async (_: unknown, { input }: { input: CreateUserInput }, ctx: Context) => {
return ctx.db.users.create({ data: input });
},
},
};
GraphQL的优势
- 无过度获取或不足获取:客户端精确请求所需的字段。
- 单一端点:所有操作通过一个URL进行。
- 强类型Schema:Schema作为活文档,支持强大的工具。
- 自省:客户端可以通过编程方式发现整个API表面。
- 订阅:通过WebSocket内置支持实时数据。
- 适合多样化客户端:移动应用可以请求最少的字段,桌面应用可以获取所有内容。
GraphQL的劣势
- 复杂性:Resolver、数据加载器、持久化查询和Schema管理增加了认知负担。
- 缓存更难:由于所有请求都是对单个端点的POST,传统HTTP缓存不起作用。需要客户端缓存(Apollo、urql)或持久化查询。
- N+1查询问题:没有数据加载器,嵌套的Resolver可能触发过多的数据库查询。
- 文件上传:不原生支持;需要多部分请求规范或单独的上传端点。
- 安全面:深层嵌套或昂贵的查询如果不实施查询深度限制和复杂度分析,可能导致拒绝服务。
tRPC:端到端类型安全
tRPC是一个TypeScript专用框架,允许你从客户端直接调用服务器函数,具有完整的类型安全性且无需代码生成。它专为前端和后端都使用TypeScript编写的monorepo项目设计。
tRPC的工作原理
tRPC将API层作为一个独立概念消除了。你在服务器上定义过程,客户端像调用本地函数一样调用它们。
服务端路由器定义:
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const user = await ctx.db.users.findUnique({
where: { id: input.id },
include: { posts: true, profile: true },
});
if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
return user;
}),
list: publicProcedure
.input(z.object({
page: z.number().default(1),
limit: z.number().min(1).max(100).default(20),
search: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const { page, limit, search } = input;
const where = search
? { name: { contains: search, mode: 'insensitive' as const } }
: {};
const [users, total] = await Promise.all([
ctx.db.users.findMany({
where,
skip: (page - 1) * limit,
take: limit,
}),
ctx.db.users.count({ where }),
]);
return { users, total, page, limit };
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.db.users.create({ data: input });
}),
update: protectedProcedure
.input(z.object({
id: z.string(),
name: z.string().min(1).max(100).optional(),
email: z.string().email().optional(),
}))
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
return ctx.db.users.update({ where: { id }, data });
}),
});
客户端使用(React):
// Client: full type safety, no code generation needed
import { trpc } from '../utils/trpc';
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = trpc.user.getById.useQuery({ id: userId });
const updateUser = trpc.user.update.useMutation({
onSuccess: () => {
// Invalidate and refetch
utils.user.getById.invalidate({ id: userId });
},
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error.message} />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}>
Update Name
</button>
</div>
);
}
每个输入、输出和错误都是完全类型化的。如果你在服务器上更改字段名,客户端会立即显示TypeScript错误。
tRPC的优势
- 端到端类型安全:服务器的更改立即反映在客户端类型中,无需代码生成。
- 最少的样板代码:没有Schema文件、API路由定义或客户端生成步骤。
- 出色的开发者体验:IDE中的自动补全、内联文档和重构支持。
- 内置输入验证:Zod Schema在运行时验证输入,在编译时提供类型。
- React Query集成:对缓存、乐观更新和实时重新获取的一流支持。
- 订阅:用于实时功能的WebSocket支持。
tRPC的劣势
- 仅限TypeScript:客户端和服务器都必须是TypeScript。不适用于Python、Go或其他后端语言。
- 以monorepo为中心:当客户端和服务器代码在一起时效果最好。团队在不同仓库中工作时不太实用。
- 不适合公共API:没有语言无关的Schema,不适合第三方使用的API。
- 客户端和服务器紧耦合:紧耦合随着团队增长可能成为维护问题。
- 较小的生态系统:与REST或GraphQL相比,教程、工具和社区资源较少。
比较表
| 特性 | REST | GraphQL | tRPC |
|---|---|---|---|
| 类型安全 | 手动(OpenAPI代码生成) | 基于Schema | 自动(TypeScript) |
| 过度获取 | 常见 | 消除 | 最小(类型化返回) |
| 不足获取 | 常见(N+1请求) | 消除 | 最小 |
| 缓存 | 优秀(HTTP原生) | 复杂(客户端) | React Query内置 |
| 学习曲线 | 低 | 中高 | 低(如果你懂TS) |
| 工具成熟度 | 优秀 | 非常好 | 好 |
| 语言支持 | 所有语言 | 所有语言 | 仅TypeScript |
| 实时 | WebSocket/SSE(独立) | 订阅 | 订阅 |
| 文件上传 | 原生 | 需要变通 | 需要变通 |
| 公共API | 理想 | 好 | 不适合 |
| 代码生成 | 可选(OpenAPI) | 经常需要 | 不需要 |
| 性能 | 好 | 好(带批处理) | 优秀(最小开销) |
| API文档 | Swagger/OpenAPI | Schema自省 | TypeScript类型 |
何时选择各方案
选择REST
- 构建被不同语言的第三方使用的公共API。
- 需要CDN或浏览器级别的HTTP缓存(如内容丰富的站点)。
- 团队中有更熟悉传统HTTP API的开发者。
- 构建跨不同语言运行时通信的微服务。
- 需要最简单的架构来处理直接的CRUD应用。
选择GraphQL
- 有多个客户端(Web、移动、TV、手表)需要来自同一API的不同数据形状。
- 数据是深度关联的,客户端经常需要在单个请求中获取嵌套数据。
- 需要与查询层紧密集成的实时功能(订阅)。
- 需要带有自省功能的自文档化API用于开发者工具。
- 构建从多个后端服务聚合数据的网关。
选择tRPC
- 整个技术栈都是TypeScript(前端和后端)。
- 在客户端和服务器代码同处的monorepo中工作。
- 重视开发者速度,需要最快的迭代速度。
- 构建API不对外暴露的内部工具或SaaS产品。
- 类型安全是首要优先级,想在编译时捕获API不匹配。
实际架构模式
混合方案:tRPC + REST
许多团队将tRPC用于内部前端到后端通信,并暴露单独的REST API用于第三方集成:
// Internal tRPC router for the dashboard
export const appRouter = router({
user: userRouter,
billing: billingRouter,
analytics: analyticsRouter,
});
// Public REST API for integrations
app.get('/api/v1/users/:id', async (req, res) => {
const user = await db.users.findUnique({
where: { id: req.params.id },
select: publicUserFields,
});
res.json(user);
});
GraphQL作为网关
GraphQL作为联合多个微服务数据的API网关效果很好:
// Gateway schema that combines multiple services
const gatewaySchema = stitchSchemas({
subschemas: [
{ schema: userServiceSchema, executor: userServiceExecutor },
{ schema: orderServiceSchema, executor: orderServiceExecutor },
{ schema: inventoryServiceSchema, executor: inventoryServiceExecutor },
],
});
REST + OpenAPI实现类型安全
如果选择REST但想要类型安全,OpenAPI代码生成可以弥补差距:
# openapi.yaml
paths:
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
# Generate typed client
npx openapi-typescript-codegen --input openapi.yaml --output src/api
// Generated client with full type safety
import { UserService } from './api';
const user = await UserService.getUser({ id: '42' });
// user is fully typed
性能考虑
REST性能
REST受益于HTTP/2多路复用和浏览器缓存。对于提供可缓存内容的读取密集型API,REST可以非常快。缺点是当需要来自多个端点的数据时需要多次往返。
GraphQL性能
GraphQL通过允许在单个请求中发送复杂查询来减少往返。但是,服务器必须解析可能复杂的查询树。没有适当的优化(数据加载器、查询复杂度限制、持久化查询),GraphQL实际上可能比REST更慢。
tRPC性能
tRPC通过HTTP上的简单JSON通信,序列化开销最小。启用请求批处理后,多个过程调用合并为单个HTTP请求,减少往返。React Query集成提供客户端缓存,进一步减少网络请求。
总结
不存在单一的最佳API方案。正确的选择取决于你的团队、技术栈和项目需求。
REST仍然是公共API、微服务通信以及使用多种语言的团队的默认选择。它的简洁性和普遍支持使其成为大多数场景的安全选择。
GraphQL在你有复杂的关系数据和需要不同数据视图的多个客户端应用时表现出色。它增加了复杂性,但对于合适的用例,其灵活性是值得的。
tRPC是TypeScript monorepo项目中开发者速度和类型安全优先时的最佳选择。它消除了整个类别的错误(API契约不匹配),并提供三者中最快的开发体验。
许多成功的团队使用这些方案的组合,在各自最适合的地方发挥其优势。重要的是理解权衡并做出有意识的决定,而不是默认使用上一个项目使用的方案。