23.01-前后端接口协作模式
要点
- 前后端协作的核心是「接口契约」,而不是「谁等谁」
- API 设计应该前端驱动(Frontend-First),而不是后端驱动
- 使用 OpenAPI/Swagger 或 Zod schema 作为单一真相源
- Mock 服务让前端可以独立开发,不依赖后端进度
内容
1. 传统前后端协作的痛点
传统的前后端协作模式通常是这样的:
- 产品经理写 PRD
- 后端设计数据库和 API
- 后端开发 API,写接口文档
- 前端等后端完成后,开始对接
- 联调,发现问题,反复修改
这种模式的问题:
- 前端被阻塞:后端没完成,前端没法开发
- 接口不符合前端需求:后端设计的 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 流程图加载中...
关键步骤:
- 前端先设计 UI:看组件需要什么数据
- 共同评审 API:前后端一起讨论接口设计
- 后端实现前,先提供 Mock:前端可以并行开发
- 使用自动化工具:OpenAPI/Zod 生成类型和文档
- 联调前做契约测试:验证 API 是否符合约定
8. 小结
前后端接口协作的关键点:
- Frontend-First:先设计前端需要什么,再设计后端 API
- 接口契约:使用 OpenAPI/Zod 作为单一真相源
- Mock 服务:前端可以独立开发,不依赖后端
- 自动生成:从 schema 生成类型、文档、客户端
- 版本管理:API 演进时保持向后兼容
一句话带走:
前后端协作的核心不是「谁等谁」,而是「共同定义接口契约,然后各自独立开发」。