API 设计与 BFF 层:REST、GraphQL、tRPC 选型与前后端协作

系统性 API 设计与 BFF(Backend for Frontend)实践:RESTful API 设计规范(HTTP 方法/状态码/资源命名/分页/版本控制)、HATEOAS、OpenAPI 规范与自动化(生成客户端/文档/Mock)、GraphQL 类型系统与查询优化(N+1/DataLoader/缓存)、tRPC 端到端类型安全、tRPC 路由与中间件、BFF 架构设计(职责边界/API 聚合/权限转发)、前后端协作模式(契约测试/API First/Mock Server)、API 版本化策略与平滑迁移、gRPC-Web 在浏览器场景的应用。附 Node.js/Go 实战。

好的 API 设计是前后端协作的契约,好的 BFF 层是产品体验的放大器。 当移动端需要精简字段、桌面端需要丰富关联、管理后台需要批量操作时,直接在数据源层做妥协会让所有人都痛苦——BFF 的存在是为了让每个客户端都能获得「刚好需要」的数据结构。


一、RESTful API 设计规范

1.1 HTTP 方法与资源命名

方法操作幂等示例
GET读取GET /users / GET /users/123
POST创建POST /users
PUT全量更新PUT /users/123
PATCH部分更新PATCH /users/123
DELETE删除DELETE /users/123
资源命名:
✅ /users              — 集合
✅ /users/123          — 单个资源
✅ /users/123/orders   — 子资源
✅ /users/123/posts?status=published&page=2&limit=20 — 查询参数

❌ /getUsers           — 动作不要放在 URL 中
❌ /user/123           — 集合用复数
❌ /users/123/delete   — 用 DELETE 方法

1.2 响应格式

{
  "data": {
    "id": "u-123",
    "name": "Alice",
    "email": "alice@example.com",
    "createdAt": "2026-01-15T08:30:00Z"
  },
  "meta": {
    "page": 1,
    "pageSize": 20,
    "total": 156,
    "totalPages": 8
  },
  "links": {
    "self": "/users/u-123",
    "orders": "/users/u-123/orders"
  }
}

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "details": {
      "userId": ["未找到 ID 为 u-999 的用户"]
    }
  }
}

1.3 分页策略

方式适用示例优点缺点
Offset传统列表?page=2&limit=20简单深页性能差、数据漂移
Cursor实时/无限滚动?cursor=eyJpZCI6MTIzfQ==无漂移、性能好无法跳页
Seek顺序数据?after_id=123&limit=20简单+可靠仅适合单一排序

1.4 版本控制

URL 路径(推荐):
  GET /v1/users
  GET /v2/users  // 破坏变更时递增

请求头(可选):
  Accept: application/vnd.api+json; version=2

不要这样:
  /api/v1.2.3/users  — 细版本无意义
  完全不做版本       — 无法演进

二、OpenAPI:API 为契约

2.1 OpenAPI 3.1 规范

# openapi.yaml
openapi: 3.1.0
info:
  title: My API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found

components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
        createdAt:
          type: string
          format: date-time

2.2 自动化工具链

# 1. 从代码生成 OpenAPI(后端)
# Go: swag, oapi-codegen
# Node.js: @nestjs/swagger, tsoa
# Python: FastAPI(原生生成)

# 2. 从 OpenAPI 生成客户端(前端)
npx openapi-typescript https://api.example.com/openapi.json -o src/api.ts

# 3. 生成 Mock Server
npx @stoplight/prism-cli mock openapi.yaml

# 4. 生成文档
npx redocly build-docs openapi.yaml -o docs.html

三、GraphQL:类型驱动的查询

3.1 Schema 设计

type User {
  id: ID!
  name: String!
  email: String!
  avatar: String
  posts(page: Int, limit: Int): PostConnection!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  tags: [String!]!
  publishedAt: DateTime
}

type PostConnection {
  edges: [Post!]!
  pageInfo: PageInfo!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

type Query {
  user(id: ID!): User
  users(filter: UserFilter, page: Int, limit: Int): [User!]!
  me: User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

input CreateUserInput {
  name: String!
  email: String!
}

3.2 查询示例

# 前端只请求需要的字段
query GetUserWithPosts($userId: ID!, $page: Int) {
  user(id: $userId) {
    id
    name
    email
    posts(page: $page, limit: 5) {
      edges {
        id
        title
        publishedAt
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

3.3 N+1 问题与 DataLoader

// ❌ N+1:查 100 个用户 → 发 100 次查询
const users = await db.users.findMany();
for (const user of users) {
  user.posts = await db.posts.findMany({ where: { authorId: user.id } });
}

// ✅ DataLoader:批量 + 缓存
import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.posts.findMany({
    where: { authorId: { in: userIds } }
  });
  // 按 userId 分组返回
  return userIds.map(id => posts.filter(p => p.authorId === id));
});

// 解析器中自动批量
const resolvers = {
  User: {
    posts: (user) => postLoader.load(user.id)
  }
};

3.4 GraphQL vs REST

维度GraphQLREST
数据获取前端决定字段后端决定响应结构
请求数单次聚合多次请求(或过度获取)
缓存需要自定义HTTP 缓存原生支持
学习曲线较陡平缓
工具链Apollo/URQL/Relayfetch/axios/SWR
适用复杂关联数据、多客户端简单 CRUD、CDN 缓存

四、tRPC:端到端类型安全

4.1 核心理念

「你的 API 路由就是 TypeScript 类型,前后端共享同一份类型定义。」

// server/router.ts(后端)
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  user: t.router({
    getById: t.procedure
      .input(z.object({ id: z.string().uuid() }))
      .query(async ({ input }) => {
        return db.user.findById(input.id);
      }),

    create: t.procedure
      .input(z.object({
        name: z.string().min(1).max(100),
        email: z.string().email()
      }))
      .mutation(async ({ input }) => {
        return db.user.create(input);
      }),
  }),
});

export type AppRouter = typeof appRouter;
// client/trpc.ts(前端)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCReact<AppRouter>();

// 使用
function UserProfile({ userId }: { userId: string }) {
  const { data } = trpc.user.getById.useQuery({ id: userId });

  // TypeScript 自动推导 data 类型 = User | undefined
  // 错误的字段名会编译报错!

  return <div>{data?.name}</div>;
}

4.2 tRPC 优势

优势说明
零类型声明无需手写 DTO/Interface/API 契约
自动填充IDE 自动补全所有路由和参数
运行时校验Zod 自动校验输入
轻量传输JSON RPC over HTTP,无 Schema 传输开销
React Query 集成自动缓存、重试、乐观更新

4.3 tRPC 适用场景

✅ 适用:
  - 全 TypeScript 栈(Node.js + React/Vue)
  - 内部项目、快速迭代
  - 对类型安全有极高要求

❌ 不适用:
  - 多语言后端(Go/Python/Java)
  - 对外开放 API(第三方消费)
  - 需要 CDN 缓存的读接口

五、BFF 架构设计

5.1 为什么需要 BFF

问题:一个后端数据源,多个前端客户端

移动端 App        → 需要精简字段、减少嵌套
桌面 Web          → 需要丰富关联、实时更新
管理后台          → 需要批量操作、复杂筛选
第三方开放平台     → 需要严格权限、 Rate Limit

直接暴露后端服务 → 所有客户端耦合在一起,互相制约

5.2 BFF 职责边界

BFF(Backend for Frontend)职责:
├── API 聚合     — 一次请求合并多个后端服务
├── 数据裁剪     — 只返回前端需要的字段
├── 格式转换     — GraphQL → REST / Protocol Buffers → JSON
├── 协议适配     — gRPC-Web ↔ gRPC
├── 认证转发     — 校验 Token,转发用户身份
├── 缓存策略     — 按客户端定制缓存
├── 错误处理     — 统一错误格式
└── 限流/降级    — 按客户端保护后端

BFF 不负责:
❌ 业务逻辑(应保持薄)
❌ 数据持久化(交给后端服务)
❌ 复杂事务(通过 Saga 模式在后端处理)

5.3 BFF 技术选型

场景技术说明
Node.js 全栈Express/Fastify + tRPC类型安全、快速开发
前端团队维护Next.js API Routes / Edge前后端一体部署
高性能要求Go / Rust低延迟、高吞吐
已有后端团队GraphQL Gateway(Apollo)由后端团队维护 BFF

5.4 Next.js Edge BFF 示例

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const token = request.headers.get('authorization');

  // 并行聚合多个后端服务
  const [user, orders, preferences] = await Promise.all([
    fetch(`${USER_SERVICE}/users/${params.id}`, { headers: { Authorization: token } }),
    fetch(`${ORDER_SERVICE}/users/${params.id}/orders?limit=5`),
    fetch(`${PREF_SERVICE}/users/${params.id}/preferences`)
  ]);

  const [userData, ordersData, prefData] = await Promise.all([
    user.json(), orders.json(), preferences.json()
  ]);

  // 裁剪和聚合:前端只需要这些
  return NextResponse.json({
    id: userData.id,
    name: userData.name,
    avatar: userData.avatar,
    recentOrders: ordersData.map((o: any) => ({
      id: o.id,
      total: o.total,
      status: o.status
    })),
    theme: prefData.theme,
    language: prefData.language
  });
}

六、前后端协作模式

6.1 API First

API First 流程:
1. 产品确定需求
2. 前后端共同设计 OpenAPI 契约
3. 用 Prism/MockServer 启动 Mock
4. 前端基于 Mock 开发 UI
5. 后端按契约实现
6. 契约测试验证一致性
7. 联调 → 上线

6.2 契约测试

// 用 Pact 做消费者驱动的契约测试
import { Pact } from '@pact-foundation/pact';

const provider = new Pact({
  consumer: 'Frontend',
  provider: 'UserService'
});

// 前端定义期望
await provider.addInteraction({
  state: 'user exists',
  uponReceiving: 'a request for user 123',
  withRequest: { method: 'GET', path: '/users/123' },
  willRespondWith: {
    status: 200,
    body: {
      id: '123',
      name: 'Alice',
      email: 'alice@example.com'
    }
  }
});

// 后端验证是否满足契约
// pact-verifier --provider-base-url http://localhost:8080 --pact-broker-base-url ...

七、选型决策树

API 范式选型:

是否全 TypeScript 栈 + 内部项目?
├── 是 → tRPC ✅
│   └── 需要第三方消费?
│       └── 是 → 加 OpenAPI 导出
├── 否 → 继续
│   客户端是否需要精确控制字段?
│   ├── 是 → GraphQL
│   │   └── N+1 严重? → DataLoader + 分页优化
│   └── 否 → REST
│       ├── CDN 缓存重要? → REST + HTTP Cache(推荐)
│       └── 简单 CRUD? → REST + OpenAPI
多客户端复杂需求?
└── 是 → BFF 层
    ├── 前端团队维护 → Next.js Edge / tRPC
    └── 后端团队维护 → GraphQL Gateway / Go BFF

八、API 设计 Checklist

检查项说明
使用资源名词,非动作/users/getUsers
HTTP 方法语义正确GET 安全幂等,POST 创建,PUT/PATCH 更新
状态码准确200/201/204/400/401/403/404/422/500
错误信息机器可读{ code, message, details }
分页支持cursor > offset(实时数据)
版本控制URL 路径 /v1/
文档自动OpenAPI + 生成器
输入校验Schema 校验(Zod/Joi)
限流Rate Limit 标头
监控响应时间、错误率、.usage

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. WebAssembly 前端工程化实践:编译链、性能对比与混合架构
  2. 现代浏览器 API 与 Web 平台能力地图
  3. 前端安全进阶:XSS、CSP、SRI 与供应链安全