23.01-前后端接口协作模式

要点

  • 前后端协作的核心是「接口契约」,而不是「谁等谁」
  • API 设计应该前端驱动(Frontend-First),而不是后端驱动
  • 使用 OpenAPI/Swagger 或 Zod schema 作为单一真相源
  • Mock 服务让前端可以独立开发,不依赖后端进度

内容

1. 传统前后端协作的痛点

传统的前后端协作模式通常是这样的:

  1. 产品经理写 PRD
  2. 后端设计数据库和 API
  3. 后端开发 API,写接口文档
  4. 前端等后端完成后,开始对接
  5. 联调,发现问题,反复修改

这种模式的问题:

  • 前端被阻塞:后端没完成,前端没法开发
  • 接口不符合前端需求:后端设计的 API 结构,前端用起来很别扭
  • 文档和实现不一致:接口文档过时,前端踩坑
  • 联调成本高:前后端都开发完了,一联调发现各种问题

2. Frontend-First 的 API 设计

Frontend-First 的核心理念:先设计前端需要什么数据,再设计后端 API

2.1 从 UI 组件出发

// 先写前端组件,看它需要什么数据
// src/components/UserProfile.tsx
function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>注册于 {formatDate(user.createdAt)}</p>
      <p>积分:{user.points}</p>
    </div>
  )
}
 
// 从这个组件可以看出,前端需要:
// - user.name (string)
// - user.email (string)
// - user.createdAt (Date)
// - user.points (number)
// 根据前端需求设计 API 响应
// GET /api/users/:id
interface UserResponse {
  id: string
  name: string
  email: string
  createdAt: string  // ISO 8601
  points: number
}

2.2 避免「数据库暴露」

❌ 错误做法:直接把数据库表结构暴露成 API

// ❌ 后端数据库驱动设计
// GET /api/users/:id
interface UserResponse {
  user_id: number           // 数据库字段名
  user_name: string         // 下划线命名
  user_email: string
  created_at: string        // 数据库时间戳
  user_points: number
  password_hash: string     // 不应该暴露
  is_deleted: boolean       // 内部状态
}

✅ 正确做法:根据前端需求设计 API

// ✅ 前端驱动设计
// GET /api/users/:id
interface UserResponse {
  id: string                // 前端友好的命名
  name: string
  email: string
  createdAt: string         // ISO 8601
  points: number
  // 不暴露 password_hash、is_deleted 等内部字段
}

3. 接口契约的三种方式

3.1 OpenAPI/Swagger

OpenAPI 是行业标准,前后端都可以从 OpenAPI spec 生成代码:

# openapi.yaml
openapi: 3.0.0
info:
  title: AI Gateway API
  version: 1.0.0
paths:
  /v1/chat/completions:
    post:
      summary: 调用 LLM
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatRequest'
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatResponse'
components:
  schemas:
    ChatRequest:
      type: object
      required:
        - messages
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/Message'
        model:
          type: string
          enum: [gpt-4, gpt-3.5-turbo]
    Message:
      type: object
      properties:
        role:
          type: string
          enum: [system, user, assistant]
        content:
          type: string
    ChatResponse:
      type: object
      properties:
        id:
          type: string
        choices:
          type: array
          items:
            type: object
            properties:
              message:
                $ref: '#/components/schemas/Message'

后端:用 Swagger 生成 Hono 路由模板

import { OpenAPIHono } from '@hono/swagger-ui'
 
const app = new OpenAPIHono()
 
app.doc('/doc', {
  openapi: '3.0.0',
  info: { title: 'AI Gateway', version: '1.0.0' },
})
 
// 从 OpenAPI spec 生成路由
app.post('/v1/chat/completions', async (c) => {
  // ...
})

前端:用 OpenAPI Generator 生成 TypeScript 客户端

npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o ./generated
// 前端直接使用生成的客户端
import { ChatApi } from './generated'
 
const api = new ChatApi()
const response = await api.chatCompletions({
  messages: [{ role: 'user', content: '你好' }],
})

3.2 Zod Schema + Hono RPC

Hono 的 RPC 客户端可以从 Zod schema 自动推导类型:

// packages/shared/schemas/chat.ts
import { z } from 'zod'
 
export const ChatRequestSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['system', 'user', 'assistant']),
    content: z.string(),
  })),
  model: z.enum(['gpt-4', 'gpt-3.5-turbo']).default('gpt-4'),
})
 
export type ChatRequest = z.infer<typeof ChatRequestSchema>
// apps/api/src/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { ChatRequestSchema } from '@shared/schemas/chat'
 
const app = new Hono()
  .post('/v1/chat/completions', zValidator('json', ChatRequestSchema), async (c) => {
    const body = c.req.valid('json')
    // body 的类型自动推导为 ChatRequest
    return c.json({ id: '123', choices: [] })
  })
 
export type AppType = typeof app
// apps/web/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
 
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)
 
// 前端调用时自动获得类型提示
const res = await api.v1.chat.completions.$post({
  json: {
    messages: [{ role: 'user', content: '你好' }],
    model: 'gpt-4',
  },
})

3.3 TypeScript 接口 + 手动同步

最简单的方式,但需要手动保持前后端一致:

// packages/shared/types/api.ts
export interface ChatRequest {
  messages: Message[]
  model: 'gpt-4' | 'gpt-3.5-turbo'
}
 
export interface ChatResponse {
  id: string
  choices: Choice[]
}
 
export interface Message {
  role: 'system' | 'user' | 'assistant'
  content: string
}

前后端都 import 这个文件,手动保证类型一致。

4. Mock 服务

前端不应该等后端开发完成才开始。使用 Mock 服务可以让前端独立开发。

4.1 MSW (Mock Service Worker)

MSW 可以在浏览器层面拦截请求,返回 Mock 数据:

// apps/web/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
 
export const handlers = [
  http.post('/api/chat/completions', async ({ request }) => {
    const body = await request.json()
 
    return HttpResponse.json({
      id: 'mock-123',
      choices: [
        {
          message: {
            role: 'assistant',
            content: `Mock 回复:你说了 ${body.messages[0].content}`,
          },
        },
      ],
    })
  }),
]
// apps/web/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
 
export const worker = setupWorker(...handlers)
// apps/web/src/main.tsx
import { worker } from './mocks/browser'
 
// 开发环境启动 Mock 服务
if (process.env.NODE_ENV === 'development') {
  worker.start()
}

4.2 Hono 内置 Mock

Hono 可以创建一个独立的 Mock 服务:

// apps/api/src/mock.ts
import { Hono } from 'hono'
 
export const mockApp = new Hono()
 
mockApp.post('/v1/chat/completions', async (c) => {
  const body = await c.req.json()
 
  return c.json({
    id: 'mock-123',
    choices: [
      {
        message: {
          role: 'assistant',
          content: `Mock 回复:你说了 ${body.messages[0].content}`,
        },
      },
    ],
  })
})
 
export default mockApp
// apps/api/src/index.ts
import { Hono } from 'hono'
import mockApp from './mock'
 
const app = new Hono()
 
// 开发环境使用 Mock
if (process.env.NODE_ENV === 'development') {
  app.route('/', mockApp)
} else {
  // 生产环境使用真实逻辑
  app.post('/v1/chat/completions', async (c) => {
    // 真实的 LLM 调用
  })
}

5. 接口版本管理

API 会演进,需要版本管理避免破坏现有客户端。

5.1 URL 版本

// v1
app.get('/v1/users/:id', (c) => {
  return c.json({ id: c.req.param('id'), name: 'Alice' })
})
 
// v2(新增字段)
app.get('/v2/users/:id', (c) => {
  return c.json({
    id: c.req.param('id'),
    name: 'Alice',
    email: '[email protected]',  // 新增
  })
})

5.2 Header 版本

app.get('/users/:id', (c) => {
  const version = c.req.header('X-API-Version') || 'v1'
 
  if (version === 'v2') {
    return c.json({
      id: c.req.param('id'),
      name: 'Alice',
      email: '[email protected]',
    })
  }
 
  return c.json({ id: c.req.param('id'), name: 'Alice' })
})

6. 接口文档自动生成

6.1 Hono + Swagger UI

import { OpenAPIHono, createRoute } from '@hono/zod-openapi'
import { SwaggerUI } from '@hono/swagger-ui'
import { z } from 'zod'
 
const app = new OpenAPIHono()
 
const route = createRoute({
  method: 'post',
  path: '/v1/chat/completions',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z.object({
            messages: z.array(z.object({
              role: z.enum(['system', 'user', 'assistant']),
              content: z.string(),
            })),
            model: z.enum(['gpt-4', 'gpt-3.5-turbo']),
          }),
        },
      },
    },
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: z.object({
            id: z.string(),
            choices: z.array(z.object({
              message: z.object({
                role: z.enum(['assistant']),
                content: z.string(),
              }),
            })),
          }),
        },
      },
    },
  },
})
 
app.openapi(route, (c) => {
  const body = c.req.valid('json')
  return c.json({
    id: '123',
    choices: [{ message: { role: 'assistant', content: '你好' } }],
  })
})
 
// 自动生成 OpenAPI spec
app.doc('/doc', {
  openapi: '3.0.0',
  info: { title: 'AI Gateway', version: '1.0.0' },
})
 
// 提供 Swagger UI
app.get('/swagger', SwaggerUI({ url: '/doc' }))

访问 /swagger 可以看到交互式 API 文档。

7. 实战:完整的前后端协作流程

流程图画布 · 115%
Mermaid 流程图加载中...

关键步骤:

  1. 前端先设计 UI:看组件需要什么数据
  2. 共同评审 API:前后端一起讨论接口设计
  3. 后端实现前,先提供 Mock:前端可以并行开发
  4. 使用自动化工具:OpenAPI/Zod 生成类型和文档
  5. 联调前做契约测试:验证 API 是否符合约定

8. 小结

前后端接口协作的关键点:

  1. Frontend-First:先设计前端需要什么,再设计后端 API
  2. 接口契约:使用 OpenAPI/Zod 作为单一真相源
  3. Mock 服务:前端可以独立开发,不依赖后端
  4. 自动生成:从 schema 生成类型、文档、客户端
  5. 版本管理:API 演进时保持向后兼容

一句话带走:

前后端协作的核心不是「谁等谁」,而是「共同定义接口契约,然后各自独立开发」。