🔌 API 设计

REST / GraphQL / tRPC / gRPC — 何时选什么,版本管理策略,认证模式,错误处理,以及从 Agent 架构中学到的 API 设计教训。

01 API 风格全景:四种主流范式

API 是你的产品与世界的接口。选错风格不会立刻致命,但会在规模增长时变成持续的技术债。这不是宗教战争——每种风格都有明确的适用场景。

🌐 REST

通用成熟

资源导向,HTTP 语义,最广泛支持。公开 API 的默认选择。

📊 GraphQL

灵活复杂

查询导向,客户端决定数据形状。复杂前端 + 多数据源的利器。

⚡ tRPC

全栈 TS类型安全

端到端类型推断,零代码生成。全栈 TypeScript 项目的最佳 DX。

🚀 gRPC

高性能服务间

Protocol Buffers,双向流。微服务内部通信的首选。

核心原则:公开 API → REST;内部前后端 → tRPC(如果全 TS)或 GraphQL;服务间 → gRPC。很多成功的 SaaS 同时使用多种风格,不同场景用不同工具。

02 REST:不死的老兵

2.1 什么是 REST

REST(Representational State Transfer)不是框架,不是库,是架构风格。它的核心思想:一切皆资源,用 HTTP 动词操作资源

REST 六大约束
约束含义实际意义
Client-Server前后端分离独立演进,前端换框架不影响 API
Stateless请求自带全部上下文水平扩展容易,无需 sticky session
Cacheable响应必须声明可缓存性ETag / Cache-Control 减少重复请求
Uniform Interface统一接口约定HTTP 动词 + 资源 URI + 状态码
Layered System客户端不知道是否直连服务器可插入 CDN / 网关 / 负载均衡
Code on Demand 可选服务器可返回可执行代码JS widget、WebAssembly 模块

2.2 RESTful API 设计实战

资源命名

# ✅ 好的设计:名词复数,层级清晰
GET    /api/v1/users              # 用户列表
GET    /api/v1/users/123          # 单个用户
GET    /api/v1/users/123/orders   # 用户 123 的订单
POST   /api/v1/users/123/orders   # 为用户 123 创建订单

# ❌ 坏的设计:动词路径,非 RESTful
GET    /api/v1/getUsers
POST   /api/v1/createOrder
POST   /api/v1/deleteUser/123

# ❌ 嵌套过深(超过 2 层就该重新设计)
GET    /api/v1/users/123/orders/456/items/789/refunds

HTTP 动词的正确用法

# CRUD 映射
GET      /api/v1/articles         # 列表(可过滤/分页)
POST     /api/v1/articles         # 创建
GET      /api/v1/articles/42      # 详情
PATCH    /api/v1/articles/42      # 部分更新(推荐)
PUT      /api/v1/articles/42      # 全量替换(少用)
DELETE   /api/v1/articles/42      # 删除

# 特殊动作(非 CRUD)
POST     /api/v1/articles/42/publish    # 发布(状态变更)
POST     /api/v1/articles/42/duplicate  # 复制(创建新资源)
POST     /api/v1/exports                # 异步导出(返回 202)

分页、过滤、排序

# 分页 — Cursor-based(推荐,适合无限滚动)
GET /api/v1/articles?cursor=eyJpZCI6MTAwfQ&limit=20

# 分页 — Offset-based(适合跳页)
GET /api/v1/articles?page=2&page_size=20

# 过滤
GET /api/v1/articles?status=published&author_id=42

# 排序
GET /api/v1/articles?sort=-created_at,title
#  - 表示降序,逗号分隔多字段

# 字段选择(sparse fieldsets)
GET /api/v1/articles?fields=id,title,created_at

# 搜索
GET /api/v1/articles?q=REST+API+design

响应格式设计

// 单个资源
{
  "data": {
    "id": "art_1a2b3c",
    "type": "article",
    "title": "API 设计指南",
    "status": "published",
    "created_at": "2025-01-15T08:30:00Z",
    "updated_at": "2025-01-16T14:20:00Z"
  }
}

// 列表(带分页元信息)
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIzfQ",
    "has_more": true,
    "total_count": 1234
  }
}

// 错误
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

2.3 REST 的隐藏成本

REST 的 N+1 问题

获取用户列表 → N 次请求获取每个用户的头像和角色。这是 REST 最被诟病的地方:

// 前端:拿到用户列表后,逐个获取关联数据
GET /api/v1/users                    // 1 次
GET /api/v1/users/1/avatar           // N 次
GET /api/v1/users/2/avatar
GET /api/v1/users/3/avatar
...

// 解决方案 1:include 参数
GET /api/v1/users?include=avatar,role

// 解决方案 2:字段展开
GET /api/v1/users?expand=avatar_url,role_name

// 解决方案 3:如果这真的是常态 → 考虑 GraphQL
过度获取 vs 欠缺获取是 REST 的经典两难。要么返回太多字段浪费带宽,要么返回太少需要额外请求。解决方案:fields 参数让客户端选择字段,或者干脆用 GraphQL。

03 GraphQL:灵活性的代价

3.1 什么是 GraphQL

GraphQL 是 Facebook 开发的查询语言,核心理念:客户端精确描述需要什么数据,服务器只返回那些数据。一个 endpoint,无限可能。

Schema 定义

type User {
  id: ID!
  email: String!
  name: String
  avatar: Image
  orders(filter: OrderFilter): [Order!]!
  created_at: DateTime!
}

type Order {
  id: ID!
  items: [OrderItem!]!
  total: Float!
  status: OrderStatus!
  user: User!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
}

input OrderFilter {
  status: OrderStatus
  created_after: DateTime
}

type Query {
  user(id: ID!): User
  users(filter: UserFilter, first: Int, after: String): UserConnection!
  order(id: ID!): Order
  me: User!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  updateOrder(id: ID!, input: UpdateOrderInput!): Order!
  cancelOrder(id: ID!): Order!
}

type Subscription {
  orderUpdated(id: ID!): Order!
  newOrder: Order!
}

客户端查询

# 精确获取:只要名字和邮箱,不要别的
query {
  user(id: "usr_abc123") {
    name
    email
    orders(first: 5) {
      edges {
        node {
          id
          total
          status
        }
      }
    }
  }
}

# 同一个 Schema,不同的查询需求
query {
  me {
    name
    avatar { url }
  }
}

3.2 GraphQL 的三大问题及解决方案

问题 1:N+1 查询(DataLoader 模式)
# 没有 DataLoader:100 个用户 → 100 次 DB 查询获取头像
query {
  users(first: 100) {
    avatar { url }   # 每个 user 触发一次 DB 查询!
  }
}

# 解决:DataLoader 批量加载
const avatarLoader = new DataLoader(async (userIds) => {
  const avatars = await db.query(
    'SELECT * FROM avatars WHERE user_id IN (?)',
    [userIds]
  );
  return userIds.map(id => avatars.find(a => a.user_id === id));
});
问题 2:深度嵌套查询(DoS 攻击)
# 恶意查询:递归嵌套耗尽服务器资源
query {
  users {          # 100 个
    orders {       # 每个 50 个 = 5000
      items {      # 每个 10 个 = 50000
        product {
          reviews { # 每个 100 个 = 5000000
            user { orders { items { ... } } }
          }
        }
      }
    }
  }
}

# 解决方案:
# 1. 查询深度限制 (depth limiting)
# 2. 复杂度分析 (query complexity analysis)
# 3. 持久化查询 (persisted queries) — 只允许预注册的查询
# 4. 超时 + 结果大小限制

# Apollo Server 示例
import { depthLimit } from 'graphql-depth-limit';
const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)],  # 最大嵌套 5 层
});
问题 3:缓存困难
# REST 可以直接用 HTTP 缓存:ETag、Cache-Control
GET /api/v1/users/123   →  200 OK, ETag: "abc", Cache-Control: max-age=300

# GraphQL 只有一个 endpoint,无法用 URL 区分缓存
POST /graphql  { query: "{ user(id: 123) { name } }" }

# 解决方案:
# 1. Apollo Client — normalized cache (按 __typename:id 缓存)
# 2. 持久化查询 — GET 请求 + hash → 可用 CDN 缓存
# 3. @cacheControl 指令 — 细粒度缓存控制

type User @cacheControl(maxAge: 300) {
  id: ID!
  name: String
  email: String @cacheControl(maxAge: 0, scope: PRIVATE)
}

3.3 何时选 GraphQL

场景选 GraphQL?原因
复杂前端,多数据源✅ 强烈推荐一个查询聚合多个微服务
移动端,带宽敏感✅ 推荐精确获取,无过度获取
公开 API⚠️ 谨慎REST 更易理解,生态更广
简单 CRUD❌ 过度REST 更简单
内部服务间通信❌ 不合适gRPC 更好
全栈 TS 项目⚠️ 考虑 tRPCGraphQL 的类型安全需要代码生成

04 tRPC:全栈 TypeScript 的终极方案

4.1 什么是 tRPC

tRPC 让你在零代码生成、零手动同步的情况下,实现前后端类型安全。后端定义 router,前端直接调用——像调用本地函数一样调用远程 API。

// ===== 后端:定义 router =====
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC().create();

// 公开路由
const publicProcedure = t.procedure;
// 需认证路由
const authedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { user: ctx.user } });
});

const appRouter = t.router({
  // 查询用户列表
  listUsers: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(20),
      cursor: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const users = await ctx.db.user.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { created_at: 'desc' },
      });
      return {
        items: users.slice(0, input.limit),
        next_cursor: users.length > input.limit ? users[users.length - 1].id : undefined,
      };
    }),

  // 创建文章(需认证)
  createArticle: authedProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(1),
      tags: z.array(z.string()).max(5).default([]),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.article.create({
        data: { ...input, author_id: ctx.user.id },
      });
    }),
});

export type AppRouter = typeof appRouter;
// ===== 前端:直接调用,完整类型推断 =====
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

const trpc = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc' })],
});

// 调用后端函数——自动补全、类型检查、编译时错误
const result = await trpc.listUsers.query({
  limit: 10,
  cursor: undefined,
});
// result.items: 自动推断为 User[]
// result.next_cursor: 自动推断为 string | undefined

const article = await trpc.createArticle.mutate({
  title: 'tRPC 入门',
  content: '端到端类型安全...',
});
// 如果写错字段名 → 编译时直接报错!

4.2 tRPC vs GraphQL 对比

维度tRPCGraphQL
类型安全✅ 原生 TS 推断,零代码生成⚠️ 需要 graphql-codegen
学习曲线✅ 会 TS 就会 tRPC❌ Schema 语言 + Resolver + DataLoader
跨语言❌ 只支持 TS/JS✅ 任何语言都有客户端库
公开 API❌ 不适合✅ 天然支持
开发速度✅ 极快,改后端前端立刻知道⚠️ 中等,需要同步 Schema
缓存⚠️ React Query / SWR⚠️ Apollo Cache / 持久化查询
生态⚠️ 较新,Next.js 生态为主✅ 成熟,社区大
Batch✅ httpBatchLink 自动合并请求⚠️ 需要 Batch Http Link
订阅✅ WebSocket 链接✅ Subscription 原生支持
决策框架:如果你是一个全栈 TS 项目(前后端都是 TypeScript),且不需要公开 API → tRPC 是不二之选。如果需要公开 API 或有多语言客户端 → GraphQL。如果两者都要 → tRPC 内部 + REST 外部。

05 gRPC:微服务内部的高速公路

5.1 什么是 gRPC

gRPC 是 Google 开源的高性能 RPC 框架,使用 Protocol Buffers 序列化,支持双向流、流控、超时、认证。它是微服务间通信的黄金标准。

Proto 定义

// order.proto
syntax = "proto3";
package order;

service OrderService {
  // 普通 RPC
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (stream Order);
  
  // 客户端流
  rpc BulkCreateOrders(stream CreateOrderRequest) returns (BulkCreateResponse);
  
  // 双向流
  rpc OrderChat(stream OrderMessage) returns (stream OrderMessage);
}

message GetOrderRequest {
  string order_id = 1;
}

message ListOrdersRequest {
  int32 page_size = 1;
  string page_token = 2;
  string filter = 3;
}

message Order {
  string id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  double total = 4;
  string status = 5;
  int64 created_at = 6;  // Unix timestamp
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

Python 服务端实现

import grpc
from concurrent import futures
import order_pb2
import order_pb2_grpc

class OrderServicer(order_pb2_grpc.OrderServiceServicer):
    def GetOrder(self, request, context):
        order = db.get_order(request.order_id)
        if not order:
            context.abort(grpc.StatusCode.NOT_FOUND, "Order not found")
        return order_pb2.Order(
            id=order.id,
            user_id=order.user_id,
            total=order.total,
            status=order.status,
        )

    def ListOrders(self, request, context):
        # 服务端流:逐个推送订单
        orders = db.list_orders(
            page_size=request.page_size,
            page_token=request.page_token,
        )
        for order in orders:
            yield order_pb2.Order(
                id=order.id,
                user_id=order.user_id,
                total=order.total,
                status=order.status,
            )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    order_pb2_grpc.add_OrderServiceServicer_to_server(
        OrderServicer(), server
    )
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

5.2 gRPC 性能对比

维度gRPCREST + JSON
序列化Protocol Buffers (二进制)JSON (文本)
payload 大小小 3-10 倍较大
序列化速度快 5-20 倍较慢
流式支持✅ 四种流模式⚠️ SSE / WebSocket
代码生成✅ 自动生成客户端❌ 手动或 OpenAPI Gen
浏览器支持❌ 需要 gRPC-Web 代理✅ 原生支持
调试⚠️ 需要专用工具✅ curl / 浏览器
人可读❌ 二进制格式✅ 文本格式
gRPC 的浏览器困境:浏览器原生不支持 HTTP/2 多路复用和 trailer,所以 gRPC 不能直接从浏览器调用。解决方案:用 grpc-gateway(gRPC → REST 代理)或 envoy(gRPC-Web 代理)暴露 RESTful 接口给前端。这意味着 gRPC 主要用于服务间通信。

06 API 版本管理

版本管理是 API 设计中最容易被忽视、但影响最深远的决策。选错策略,未来的每次变更都是噩梦。

6.1 四种版本管理策略

策略方式优点缺点适用
URI 版本/api/v1/简单直观URL 变更,SEO 影响公开 API 最常见
Header 版本Accept: application/vnd.api.v2+jsonURL 不变不直观,调试难GitHub API 式
查询参数?version=2简单缓存问题不推荐
无版本 (Evolvability)只增不删,向后兼容最优雅需要强纪律Stripe API 式

推荐:URI 版本 + 向后兼容变更

# 大版本变更:URI
/api/v1/users   →  /api/v2/users

# v2 内的向后兼容变更:不改版本号
# ✅ 新增可选字段
# ✅ 新增 endpoint
# ✅ 新增查询参数
# ❌ 删除字段 → 需要新大版本
# ❌ 改变字段类型 → 需要新大版本
# ❌ 重命名字段 → 需要新大版本

# v1 → v2 迁移期:同时维护两个版本
# 迁移时间:至少 6 个月
# 通知方式:响应头 Sunset: Sat, 1 Nov 2025 00:00:00 GMT
# 通知方式:响应头 Deprecation: true

6.2 向后兼容变更的实战技巧

// ✅ 好的做法:新增字段,旧客户端忽略
// v1 响应
{ "id": 1, "name": "Alice" }
// v1 + 新字段(向后兼容)
{ "id": 1, "name": "Alice", "avatar_url": "..." }

// ❌ 坏的做法:重命名字段
// 旧
{ "name": "Alice" }
// 新(破坏性变更!)
{ "full_name": "Alice" }

// ✅ 修复:同时返回两个字段,旧字段标记 deprecated
{ "name": "Alice", "full_name": "Alice" }
// 几个月后移除 name 字段,升版本到 v2

07 认证与授权模式

API 安全的核心问题:你是谁(认证)+ 你能做什么(授权)。这部分与 认证授权 模块深度关联,这里聚焦 API 层面的实现。

7.1 API 认证方式对比

方式机制优点缺点适用
API KeyHeader / Query简单无用户概念,无法过期服务间、公开 API
JWTBearer Token无状态,自包含无法即时吊销前后端分离
Session Cookie服务端 Session安全,可即时吊销有状态,扩展复杂传统 Web 应用
OAuth2授权码 / 客户端凭证第三方授权复杂第三方登录、API 授权
mTLS双向 TLS 证书最强安全证书管理复杂服务间、零信任

JWT 实战:最佳实践

// JWT 结构
// Header: { alg: "RS256", typ: "JWT" }
// Payload: { sub: "usr_abc123", exp: 1700000000, iat: 1699999000, scope: "read write" }
// Signature: RS256(header.payload, private_key)

// Access Token + Refresh Token 双令牌模式
const accessToken = jwtSign(
  { sub: user.id, scope: 'read write' },
  { expiresIn: '15m' }   // 短命:15 分钟
);
const refreshToken = jwtSign(
  { sub: user.id, type: 'refresh' },
  { expiresIn: '7d' }     // 长命:7 天
);

// 中间件:验证 Access Token
function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Missing token' });

  try {
    const decoded = jwtVerify(token, PUBLIC_KEY, { algorithms: ['RS256'] });

    // 检查 Token 是否被吊销(黑名单)
    const isRevoked = await redis.get(`revoked:${decoded.jti}`);
    if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}
JWT 的致命陷阱:
  • 无法即时吊销 — JWT 签发后在过期前一直有效。解决方案:短命 access token + Redis 黑名单
  • Payload 可被解码 — JWT 只是签名,不是加密。不要放敏感信息(密码、密钥)
  • 算法混淆攻击 — 永远显式指定 algorithms: ['RS256'],不要用 none

7.2 API Key 设计

// API Key 格式
// 前缀 + 随机部分,方便识别来源
sk_live_1a2b3c4d5e6f7g8h9i0j   // Stripe 风格
key_xxxx_yyyy_zzzz              // 自定义风格

// 存储:只存 hash,不存明文
const crypto = require('crypto');
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');

// 请求时验证
async function validateApiKey(key) {
  const hash = sha256(key);
  const apiKeyRecord = await db.apiKey.findUnique({ where: { hash } });
  if (!apiKeyRecord) return null;
  if (apiKeyRecord.expires_at && apiKeyRecord.expires_at < new Date()) return null;
  return apiKeyRecord;
}

08 错误处理设计

好的错误处理让 API 从"能用"升级到"好用"。差的错误处理让集成者痛苦不堪。

8.1 错误响应标准格式

// 标准错误响应(RFC 7807 inspired)
{
  "error": {
    "type": "https://example.com/errors/validation-error",
    "title": "请求验证失败",
    "status": 400,
    "detail": "邮箱格式不正确",
    "instance": "/api/v1/users",
    "request_id": "req_abc123",
    "timestamp": "2025-01-15T08:30:00Z",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确",
        "value": "not-an-email"
      }
    ]
  }
}

8.2 HTTP 状态码的正确使用

状态码含义何时使用
200OKGET 成功,PUT/PATCH 更新成功
201CreatedPOST 创建成功,返回新资源
202Accepted异步操作已接受(导出、批量处理)
204No ContentDELETE 成功,无返回体
400Bad Request请求参数错误、验证失败
401Unauthorized未认证(没 token 或 token 无效)
403Forbidden已认证但无权限
404Not Found资源不存在
409Conflict冲突(重复创建、版本冲突)
422Unprocessable Entity格式正确但语义错误
429Too Many Requests限流,返回 Retry-After
500Internal Server Error服务器错误,不暴露内部信息
503Service Unavailable维护、过载,返回 Retry-After
关键区分:401 是"我不知道你是谁"(认证问题),403 是"我知道你是谁但你不能做这个"(授权问题)。不要混淆!

8.3 tRPC 错误处理

import { TRPCError } from '@trpc/server';

// 抛出类型安全错误
throw new TRPCError({
  code: 'NOT_FOUND',           // 自动映射到 HTTP 404
  message: '文章不存在',
  cause: originalError,
});

// 错误码映射
// PARSE_ERROR       → 400
// BAD_REQUEST       → 400
// UNAUTHORIZED      → 401
// FORBIDDEN         → 403
// NOT_FOUND         → 404
// METHOD_NOT_SUPPORTED → 405
// TIMEOUT           → 408
// CONFLICT          → 409
// PRECONDITION_FAILED → 412
// PAYLOAD_TOO_LARGE → 413
// TOO_MANY_REQUESTS → 429
// INTERNAL_SERVER_ERROR → 500

09 限流与配额

每个公开 API 都需要限流。没有限流 = 一个恶意用户就能打垮你的服务。

9.1 限流算法对比

算法原理优点缺点适用
固定窗口每 N 秒计数器重置简单窗口边界突刺简单场景
滑动窗口最近 N 秒的请求数平滑内存消耗大精准限流
令牌桶固定速率放入令牌允许突发实现稍复杂API 限流首选
漏桶固定速率处理请求均匀输出不允许突发流量整形

令牌桶实现 (Redis + Lua)

-- rate_limit.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])   -- 桶容量
local refill_rate = tonumber(ARGV[2]) -- 每秒补充令牌数
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])  -- 本次请求令牌数

local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])

if tokens == nil then
  tokens = capacity
  last_refill = now
end

-- 补充令牌
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * refill_rate)

-- 消耗令牌
local allowed = false
if new_tokens >= requested then
  new_tokens = new_tokens - requested
  allowed = true
end

redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)

return { allowed and 1 or 0, math.floor(new_tokens) }

9.2 限流响应头

# 响应头告诉客户端限流状态
X-RateLimit-Limit: 100          # 窗口内允许的请求数
X-RateLimit-Remaining: 67       # 剩余请求数
X-RateLimit-Reset: 1700001000   # 窗口重置时间 (Unix)
Retry-After: 30                 # 被限流时,多久后重试 (秒)

10 从 Agent 架构学到的 API 设计

在研究 20 个 AI Agent 框架时,我们发现 API 设计有一些独特的挑战和模式,值得所有 SaaS 开发者学习。

10.1 流式响应是刚需

LLM 生成是流式的,API 必须支持。这不只是 AI 场景——任何长耗时操作都应该考虑流式。

// Server-Sent Events (SSE) — 最简单的流式方案
// 服务端
app.get('/api/v1/chat/stream', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const stream = await llm.chat({
    messages: req.body.messages,
    stream: true,
  });

  for await (const chunk of stream) {
    res.write(`data: ${JSON.stringify(chunk)}\n\n`);
  }

  res.write('data: [DONE]\n\n');
  res.end();
});

// 客户端
const eventSource = new EventSource('/api/v1/chat/stream');
eventSource.onmessage = (event) => {
  if (event.data === '[DONE]') return;
  const chunk = JSON.parse(event.data);
  appendToken(chunk.content);
};

10.2 工具调用 API(Function Calling)

Agent 需要通过 API 调用工具。OpenAI 的 Function Calling 已经成为事实标准:

// 工具定义
{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "获取指定城市的天气信息",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "城市名称" },
            "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
          },
          "required": ["city"]
        }
      }
    }
  ]
}

// LLM 返回工具调用请求
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": '{"city":"北京","unit":"celsius"}'
        }
      }]
    }
  }]
}
从 Agent 研究中链接:OpenClaw 的 工具系统实现了完整的 Policy Pipeline,Codex CLI 的三级审批机制也值得参考。详见 工具系统深度分析

11 API 文档与可观测性

11.1 OpenAPI / Swagger

// openapi.yaml
openapi: 3.1.0
info:
  title: SaaS Platform API
  version: 1.0.0
paths:
  /api/v1/users:
    get:
      summary: 获取用户列表
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'

11.2 自动文档工具

文档工具推荐
工具语言方式特色
Swagger UI多语言OpenAPI Spec可交互文档,行业标准
Scalar多语言OpenAPI Spec现代 UI,比 Swagger 好看
Redoc多语言OpenAPI Spec三栏布局,适合阅读
tRPC DocsTS自动推断零配置,自动生成
FastAPI SwaggerPython装饰器自动生成 OpenAPI + Swagger UI
Mintlify多语言MDX最美文档站,付费

11.3 API 可观测性

// 请求日志格式
{
  "request_id": "req_abc123",
  "method": "GET",
  "path": "/api/v1/users",
  "status": 200,
  "duration_ms": 45,
  "user_id": "usr_xyz",
  "ip": "1.2.3.4",
  "user_agent": "Mozilla/5.0...",
  "timestamp": "2025-01-15T08:30:00.123Z"
}

// 响应头注入可追踪信息
X-Request-Id: req_abc123       // 每个请求唯一 ID
X-Response-Time: 45ms          // 响应耗时
X-RateLimit-Limit: 100         // 限流信息
X-RateLimit-Remaining: 67

12 决策框架:选哪种 API 风格?

🎯 API 风格决策树
Q1: 客户端是什么?
├─ 浏览器前端 → Q2
├─ 移动端 App → Q2
├─ 其他微服务 → gRPC
└─ 第三方开发者 → REST

Q2: 前后端都是 TypeScript 吗?
├─ 是 → tRPC(内部) + REST(外部,如需要)
└─ 否 → Q3

Q3: 数据获取复杂度?
├─ 简单 CRUD → REST
├─ 多关联,前端需要灵活组合 → GraphQL
└─ 介于之间 → REST + include/expand 参数

Q4: 需要同时服务多种客户端?
├─ 是 → GraphQL(BFF 模式)
└─ 否 → REST
实用建议:大多数 SaaS 项目的最佳起点是 REST。当你确切遇到 REST 的痛点时(N+1、过度获取、多客户端),再引入 GraphQL 或 tRPC。不要 premature optimization——先跑起来,再优化。

13 混合架构实战:REST + tRPC + gRPC

真实世界中,很多成功的 SaaS 同时使用多种 API 风格。这是一个典型的混合架构:

🌐 浏览器 🔗 API Gateway (Nginx/Envoy)

                     📐 REST /api/v1/* 🌍 公开 API + 第三方
                     ⚡ tRPC /api/trpc/* 🖥️ 内部前端
                     🚀 gRPC :50051 ⚙️ 微服务间通信
// Next.js App Router 混合路由
// app/api/v1/[...path]/route.ts  → REST handler
// app/api/trpc/[trpc]/route.ts    → tRPC handler

// Nginx 路由配置
# /api/v1/*  → Node.js REST server
# /api/trpc/* → tRPC server
# 内部 gRPC 不暴露,只在 K8s 集群内通信

server {
  location /api/v1/ {
    proxy_pass http://rest-server:3001;
  }
  location /api/trpc/ {
    proxy_pass http://trpc-server:3002;
  }
  # gRPC 服务不在 Nginx 暴露
}

14 检查清单与替代方案

14.1 API 设计检查清单

检查项
API 有版本管理策略
所有 endpoint 都有认证保护(除公开的)
错误响应格式统一
列表 endpoint 支持分页(cursor 优先)
有限流保护
有 OpenAPI 文档
敏感操作有二次确认
请求有 Request-Id 可追踪
CORS 配置正确
长操作有异步模式(202 + 轮询/Webhook)
响应字段可选择性返回(fields 参数)
大列表默认分页,不会一次返回全部

14.2 替代方案速查

如果你不想用…考虑替代原因
RESTtRPC (全 TS) / GraphQL (复杂查询)更少 boilerplate / 更灵活
GraphQLREST + include 参数 / tRPC减少复杂度 / 更好的类型安全
tRPCREST + OpenAPI (需公开 API)跨语言支持
gRPCREST (简单场景) / tRPC (TS 全栈)更简单 / 更好的浏览器支持
Swagger UIScalar / Redoc更好的 UI
自建限流Upstash Ratelimit / Cloudflare Rate Limiting托管服务,免运维

15 关键链接