阶段五:实战项目
OpenAPI(原名Swagger)是REST API描述的事实标准。一个好的OpenAPI文档就是API的完整契约——自动生成文档、客户端SDK、服务端存根、测试用例。本课从OpenAPI 3.1规范到Swagger UI集成,从设计先行到代码生成,全面掌握API文档工程化。
// openapi.yaml - 完整OpenAPI文档
openapi: "3.1.0"
info:
title: 电商API
description: |
电商平台RESTful API文档。
## 认证方式
所有需要认证的接口使用Bearer Token:
```
Authorization: Bearer <token>
```
## 速率限制
- 普通用户: 100请求/分钟
- 高级用户: 1000请求/分钟
version: "2.0.0"
contact:
name: API支持团队
email: api@example.com
url: https://docs.example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.example.com/v2
description: 生产环境
- url: https://staging-api.example.com/v2
description: 预发布环境
- url: http://localhost:3000/v2
description: 本地开发
tags:
- name: Products
description: 商品管理
- name: Orders
description: 订单管理
- name: Auth
description: 认证授权
# ===== 安全方案 =====
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
read: 读取权限
write: 写入权限
# ===== 可复用参数 =====
parameters:
PageParam:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
ProductIdParam:
name: id
in: path
required: true
schema:
type: integer
description: 商品ID
# ===== 可复用Schema =====
schemas:
Product:
type: object
required: [id, name, price, category, status]
properties:
id:
type: integer
example: 1
name:
type: string
minLength: 1
maxLength: 200
example: 机械键盘
price:
type: number
format: double
minimum: 0
exclusiveMinimum: true
example: 599.00
category:
type: string
enum: [电子产品, 服装, 食品, 图书, 家居]
example: 电子产品
stock:
type: integer
minimum: 0
example: 120
status:
type: string
enum: [active, inactive, discontinued]
example: active
tags:
type: array
items:
type: string
maxItems: 10
example: [外设, 热销]
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateProductRequest:
type: object
required: [name, price, category]
properties:
name:
type: string
minLength: 1
maxLength: 200
price:
type: number
format: double
minimum: 0
exclusiveMinimum: true
category:
type: string
enum: [电子产品, 服装, 食品, 图书, 家居]
stock:
type: integer
minimum: 0
default: 0
tags:
type: array
items:
type: string
maxItems: 10
Error:
type: object
required: [success, error]
properties:
success:
type: boolean
example: false
error:
type: object
required: [code, message]
properties:
code:
type: string
example: VALIDATION_ERROR
message:
type: string
example: 请求数据验证失败
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
hasNext:
type: boolean
hasPrev:
type: boolean
# ===== 可复用响应 =====
responses:
BadRequest:
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
success: false
error:
code: VALIDATION_ERROR
message: 请求数据验证失败
Unauthorized:
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: 资源不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: 请求频率超限
headers:
Retry-After:
schema:
type: integer
description: 重试等待秒数
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ===== 路由 =====
paths:
/products:
get:
summary: 获取商品列表
description: 支持过滤、排序和分页
tags: [Products]
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/LimitParam'
- name: category
in: query
schema:
type: string
description: 按分类过滤
- name: minPrice
in: query
schema:
type: number
description: 最低价格
- name: sort
in: query
schema:
type: string
enum: [price, -price, name, -name, createdAt, -createdAt]
description: 排序字段(-前缀降序)
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/Product'
pagination:
$ref: '#/components/schemas/Pagination'
'400':
$ref: '#/components/responses/BadRequest'
post:
summary: 创建商品
tags: [Products]
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductRequest'
responses:
'201':
description: 创建成功
headers:
Location:
schema:
type: string
description: 新资源的URL
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
description: 商品已存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/products/{id}:
get:
summary: 获取商品详情
tags: [Products]
parameters:
- $ref: '#/components/parameters/ProductIdParam'
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
$ref: '#/components/schemas/Product'
'404':
$ref: '#/components/responses/NotFound'
patch:
summary: 部分更新商品
tags: [Products]
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/ProductIdParam'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
price:
type: number
stock:
type: integer
status:
type: string
enum: [active, inactive]
responses:
'200':
description: 更新成功
'404':
$ref: '#/components/responses/NotFound'
delete:
summary: 删除商品
tags: [Products]
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/ProductIdParam'
responses:
'204':
description: 删除成功
'404':
$ref: '#/components/responses/NotFound'
// swagger-server.js - 带Swagger UI的API服务
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const yaml = require('yaml');
const fs = require('fs');
const app = express();
app.use(express.json());
// 加载OpenAPI文档
const openApiDoc = yaml.parse(fs.readFileSync('./openapi.yaml', 'utf8'));
// Swagger UI
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDoc, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: '电商API文档',
}));
// API规范端点(供工具消费)
app.get('/openapi.json', (req, res) => res.json(openApiDoc));
app.get('/openapi.yaml', (req, res) => {
res.setHeader('Content-Type', 'text/yaml');
res.send(fs.readFileSync('./openapi.yaml'));
});
// 数据
const products = [
{ id: 1, name: '机械键盘', price: 599, category: '电子产品', stock: 120, status: 'active', tags: ['外设'], createdAt: '2025-01-01', updatedAt: '2025-01-15' },
{ id: 2, name: '无线鼠标', price: 199, category: '电子产品', stock: 250, status: 'active', tags: ['外设'], createdAt: '2025-01-02', updatedAt: '2025-01-14' },
{ id: 3, name: '设计模式', price: 69, category: '图书', stock: 500, status: 'active', tags: ['编程'], createdAt: '2025-01-03', updatedAt: '2025-01-03' },
];
let nextId = 4;
// ===== 路由(与OpenAPI文档一一对应)=====
app.get('/v2/products', (req, res) => {
const { page = 1, limit = 20, category, minPrice, sort } = req.query;
let data = [...products];
if (category) data = data.filter(p => p.category === category);
if (minPrice) data = data.filter(p => p.price >= Number(minPrice));
if (sort) {
const desc = sort.startsWith('-');
const field = desc ? sort.slice(1) : sort;
data.sort((a, b) => desc ? (b[field] > a[field] ? 1 : -1) : (a[field] > b[field] ? 1 : -1));
}
const p = Math.max(1, parseInt(page));
const l = Math.min(100, parseInt(limit));
const total = data.length;
data = data.slice((p - 1) * l, p * l);
res.json({ success: true, data, pagination: { page: p, limit: l, total, totalPages: Math.ceil(total / l), hasNext: p * l < total, hasPrev: p > 1 } });
});
app.get('/v2/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `商品 ${req.params.id} 不存在` } });
res.json({ success: true, data: product });
});
app.post('/v2/products', (req, res) => {
const { name, price, category, stock, tags } = req.body;
if (!name || price === undefined || !category) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: 'name, price, category为必填项' } });
}
const product = { id: nextId++, name, price, category, stock: stock || 0, status: 'active', tags: tags || [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
products.push(product);
res.setHeader('Location', `/v2/products/${product.id}`);
res.status(201).json({ success: true, data: product });
});
app.patch('/v2/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `商品 ${req.params.id} 不存在` } });
Object.assign(product, req.body, { updatedAt: new Date().toISOString() });
res.json({ success: true, data: product });
});
app.delete('/v2/products/:id', (req, res) => {
const idx = products.findIndex(p => p.id === parseInt(req.params.id));
if (idx === -1) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `商品 ${req.params.id} 不存在` } });
products.splice(idx, 1);
res.status(204).end();
});
app.listen(3000, () => {
console.log('📚 API文档服务运行在 http://localhost:3000');
console.log('📖 Swagger UI: http://localhost:3000/docs');
console.log('📋 OpenAPI规范: http://localhost:3000/openapi.json');
});
# 安装依赖
npm install express swagger-ui-express yaml
# 启动服务
node swagger-server.js
# 访问Swagger UI
# 浏览器打开 http://localhost:3000/docs
# 用curl测试(与文档描述一致)
curl -s http://localhost:3000/v2/products | jq .
curl -s http://localhost:3000/v2/products?category=图书 | jq .
curl -s -X POST http://localhost:3000/v2/products \
-H "Content-Type: application/json" \
-d '{"name":"新商品","price":99,"category":"图书"}' | jq .
{
"success": true,
"data": {"id":4,"name":"新商品","price":99,"category":"图书","stock":0,"status":"active","tags":[]}
}
在OpenAPI文档中添加BearerAuth安全方案,标记需要认证的端点,实现JWT中间件。
使用openapi-generator从OpenAPI文档生成TypeScript客户端SDK:npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./sdk