api-contract-normalizer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Contract Normalizer
API 契约标准化工具
Standardize API contracts across all endpoints for consistency and developer experience.
统一所有接口的API契约,提升一致性与开发者体验。
Core Workflow
核心工作流程
- Audit existing APIs: Document current inconsistencies
- Define standards: Response format, pagination, errors, status codes
- Create shared types: TypeScript interfaces for all contracts
- Build middleware: Normalize responses automatically
- Document contract: OpenAPI spec with examples
- Migration plan: Phased rollout strategy
- Versioning: API version strategy
- 审计现有API:记录当前存在的不一致问题
- 定义标准:响应格式、分页规则、错误结构、状态码
- 创建共享类型:为所有契约定义TypeScript接口
- 构建中间件:自动归一化响应内容
- 文档化契约:包含示例的OpenAPI规范
- 迁移计划:分阶段落地策略
- 版本控制:API版本管理方案
Standard Response Envelope
标准响应信封
typescript
// types/api-contract.ts
export interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: ApiError;
meta?: ResponseMeta;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[] | string>;
trace_id?: string;
}
export interface ResponseMeta {
timestamp: string;
request_id: string;
version: string;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
meta: ResponseMeta & PaginationMeta;
}
export interface PaginationMeta {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}typescript
// types/api-contract.ts
export interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: ApiError;
meta?: ResponseMeta;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[] | string>;
trace_id?: string;
}
export interface ResponseMeta {
timestamp: string;
request_id: string;
version: string;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
meta: ResponseMeta & PaginationMeta;
}
export interface PaginationMeta {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}Pagination Standards
分页标准
typescript
// Standard pagination query params
interface PaginationQuery {
page: number; // 1-indexed, default: 1
limit: number; // default: 10, max: 100
sort_by?: string; // field name
sort_order?: 'asc' | 'desc'; // default: 'desc'
}
// Standard pagination response
{
"success": true,
"data": [...],
"meta": {
"page": 1,
"limit": 10,
"total": 156,
"total_pages": 16,
"has_next": true,
"has_prev": false
}
}
// Cursor-based pagination (for large datasets)
interface CursorPaginationQuery {
cursor?: string;
limit: number;
}
interface CursorPaginationMeta {
next_cursor?: string;
prev_cursor?: string;
has_more: boolean;
}typescript
// 标准分页查询参数
interface PaginationQuery {
page: number; // 从1开始计数,默认值:1
limit: number; // 默认值:10,最大值:100
sort_by?: string; // 字段名称
sort_order?: 'asc' | 'desc'; // 默认值:'desc'
}
// 标准分页响应
{
"success": true,
"data": [...],
"meta": {
"page": 1,
"limit": 10,
"total": 156,
"total_pages": 16,
"has_next": true,
"has_prev": false
}
}
// 基于游标分页(适用于大数据集)
interface CursorPaginationQuery {
cursor?: string;
limit: number;
}
interface CursorPaginationMeta {
next_cursor?: string;
prev_cursor?: string;
has_more: boolean;
}Error Standards
错误标准
typescript
// Error taxonomy
export enum ErrorCode {
// Client errors (4xx)
VALIDATION_ERROR = 'VALIDATION_ERROR',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
NOT_FOUND = 'NOT_FOUND',
CONFLICT = 'CONFLICT',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
// Server errors (5xx)
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
TIMEOUT = 'TIMEOUT',
}
// Error to HTTP status mapping
export const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
VALIDATION_ERROR: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
RATE_LIMIT_EXCEEDED: 429,
INTERNAL_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
TIMEOUT: 504,
};
// Standard error responses
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": {
"email": ["Invalid email format"],
"age": ["Must be at least 18"]
},
"trace_id": "abc123"
}
}typescript
// 错误分类
export enum ErrorCode {
// 客户端错误(4xx)
VALIDATION_ERROR = 'VALIDATION_ERROR',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
NOT_FOUND = 'NOT_FOUND',
CONFLICT = 'CONFLICT',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
// 服务端错误(5xx)
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
TIMEOUT = 'TIMEOUT',
}
// 错误与HTTP状态码映射
export const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
VALIDATION_ERROR: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
RATE_LIMIT_EXCEEDED: 429,
INTERNAL_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
TIMEOUT: 504,
};
// 标准错误响应
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": {
"email": ["Invalid email format"],
"age": ["Must be at least 18"]
},
"trace_id": "abc123"
}
}Response Normalization Middleware
响应归一化中间件
typescript
// middleware/normalize-response.ts
import { Request, Response, NextFunction } from "express";
export function normalizeResponse() {
return (req: Request, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = function (data: any) {
// Already normalized
if (data.success !== undefined) {
return originalJson(data);
}
// Normalize success response
const normalized: ApiResponse = {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
request_id: req.id,
version: "v1",
},
};
return originalJson(normalized);
};
next();
};
}
// Error normalization middleware
export function normalizeError() {
return (err: Error, req: Request, res: Response, next: NextFunction) => {
const error: ApiError = {
code: err.name || "INTERNAL_ERROR",
message: err.message || "An unexpected error occurred",
trace_id: req.id,
};
if (err instanceof ValidationError) {
error.details = err.details;
}
const statusCode = ERROR_STATUS_MAP[error.code] || 500;
res.status(statusCode).json({
success: false,
error,
meta: {
timestamp: new Date().toISOString(),
request_id: req.id,
version: "v1",
},
});
};
}typescript
// middleware/normalize-response.ts
import { Request, Response, NextFunction } from "express";
export function normalizeResponse() {
return (req: Request, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = function (data: any) {
// 已归一化的响应直接返回
if (data.success !== undefined) {
return originalJson(data);
}
// 归一化成功响应
const normalized: ApiResponse = {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
request_id: req.id,
version: "v1",
},
};
return originalJson(normalized);
};
next();
};
}
// 错误归一化中间件
export function normalizeError() {
return (err: Error, req: Request, res: Response, next: NextFunction) => {
const error: ApiError = {
code: err.name || "INTERNAL_ERROR",
message: err.message || "An unexpected error occurred",
trace_id: req.id,
};
if (err instanceof ValidationError) {
error.details = err.details;
}
const statusCode = ERROR_STATUS_MAP[error.code] || 500;
res.status(statusCode).json({
success: false,
error,
meta: {
timestamp: new Date().toISOString(),
request_id: req.id,
version: "v1",
},
});
};
}Status Code Standards
状态码标准
typescript
// Standard status codes by operation
const STATUS_CODES = {
// Success
OK: 200, // GET, PUT, PATCH success
CREATED: 201, // POST success
NO_CONTENT: 204, // DELETE success
// Client errors
BAD_REQUEST: 400, // Validation errors
UNAUTHORIZED: 401, // Missing/invalid auth
FORBIDDEN: 403, // Insufficient permissions
NOT_FOUND: 404, // Resource not found
CONFLICT: 409, // Duplicate/conflict
UNPROCESSABLE: 422, // Semantic errors
TOO_MANY_REQUESTS: 429, // Rate limit
// Server errors
INTERNAL_ERROR: 500, // Unexpected errors
SERVICE_UNAVAILABLE: 503, // Temporarily down
GATEWAY_TIMEOUT: 504, // Upstream timeout
};typescript
// 按操作类型划分的标准状态码
const STATUS_CODES = {
// 成功响应
OK: 200, // GET、PUT、PATCH请求成功
CREATED: 201, // POST请求成功
NO_CONTENT: 204, // DELETE请求成功
// 客户端错误
BAD_REQUEST: 400, // 验证错误
UNAUTHORIZED: 401, // 缺少/无效的身份验证
FORBIDDEN: 403, // 权限不足
NOT_FOUND: 404, // 资源不存在
CONFLICT: 409, // 重复/冲突
UNPROCESSABLE: 422, // 语义错误
TOO_MANY_REQUESTS: 429, // 触发速率限制
// 服务端错误
INTERNAL_ERROR: 500, // 意外错误
SERVICE_UNAVAILABLE: 503, // 服务暂时不可用
GATEWAY_TIMEOUT: 504, // 上游服务超时
};Versioning Strategy
版本控制策略
typescript
// URL versioning (recommended)
/api/v1/users
/api/v2/users
// Header versioning
Accept: application/vnd.api.v1+json
// Query param versioning (not recommended)
/api/users?version=1
// Version middleware
export function apiVersion(version: string) {
return (req: Request, res: Response, next: NextFunction) => {
req.apiVersion = version;
res.setHeader('X-API-Version', version);
next();
};
}
// Route versioning
app.use('/api/v1', apiVersion('v1'), v1Router);
app.use('/api/v2', apiVersion('v2'), v2Router);typescript
// URL版本控制(推荐)
/api/v1/users
/api/v2/users
// 请求头版本控制
Accept: application/vnd.api.v1+json
// 查询参数版本控制(不推荐)
/api/users?version=1
// 版本控制中间件
export function apiVersion(version: string) {
return (req: Request, res: Response, next: NextFunction) => {
req.apiVersion = version;
res.setHeader('X-API-Version', version);
next();
};
}
// 路由版本控制
app.use('/api/v1', apiVersion('v1'), v1Router);
app.use('/api/v2', apiVersion('v2'), v2Router);Migration Strategy
迁移策略
markdown
undefinedmarkdown
undefinedAPI Contract Migration Plan
API 契约迁移计划
Phase 1: Add Normalization (Week 1-2)
阶段1:部署归一化中间件(第1-2周)
- Deploy normalization middleware
- Run alongside existing responses
- Monitor for issues
- No breaking changes yet
- 部署响应归一化中间件
- 与现有响应模式并行运行
- 监控运行问题
- 暂不引入破坏性变更
Phase 2: Deprecation Notice (Week 3-4)
阶段2:发布弃用通知(第3-4周)
- Add deprecation headers
- Update documentation
- Notify API consumers
- Provide migration guide
- 添加弃用请求头
- 更新文档内容
- 通知API消费者
- 提供迁移指南
Phase 3: Dual Format Support (Week 5-8)
阶段3:双格式支持(第5-8周)
- Support both old and new formats
- Add ?format=v2 query param
- Track adoption metrics
- Help consumers migrate
- 同时支持新旧两种响应格式
- 添加?format=v2查询参数
- 跟踪用户采用情况
- 协助消费者完成迁移
Phase 4: Switch Default (Week 9-10)
阶段4:切换默认格式(第9-10周)
- New format becomes default
- Old format requires ?format=v1
- Final migration reminders
- Extended support period
- 将新格式设为默认
- 旧格式需通过?format=v1指定
- 发送最终迁移提醒
- 延长支持周期
Phase 5: Remove Old Format (Week 12+)
阶段5:移除旧格式(第12周及以后)
- Remove old format support
- Clean up legacy code
- Update all documentation
- Celebrate consistency! 🎉
undefined- 移除旧格式支持
- 清理遗留代码
- 更新所有文档
- 庆祝API一致性达成!🎉
undefinedContract Documentation
契约文档
yaml
undefinedyaml
undefinedopenapi.yaml
openapi.yaml
openapi: 3.0.0
info:
title: Standardized API
version: 1.0.0
description: All endpoints follow this contract
components:
schemas:
ApiResponse:
type: object
required: [success]
properties:
success:
type: boolean
data:
type: object
error:
$ref: "#/components/schemas/ApiError"
meta:
$ref: "#/components/schemas/ResponseMeta"
ApiError:
type: object
required: [code, message]
properties:
code:
type: string
enum: [VALIDATION_ERROR, UNAUTHORIZED, ...]
message:
type: string
details:
type: object
additionalProperties: true
trace_id:
type: string
PaginationMeta:
type: object
required: [page, limit, total, total_pages]
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
total_pages: { type: integer }
has_next: { type: boolean }
has_prev: { type: boolean }undefinedopenapi: 3.0.0
info:
title: Standardized API
version: 1.0.0
description: All endpoints follow this contract
components:
schemas:
ApiResponse:
type: object
required: [success]
properties:
success:
type: boolean
data:
type: object
error:
$ref: "#/components/schemas/ApiError"
meta:
$ref: "#/components/schemas/ResponseMeta"
ApiError:
type: object
required: [code, message]
properties:
code:
type: string
enum: [VALIDATION_ERROR, UNAUTHORIZED, ...]
message:
type: string
details:
type: object
additionalProperties: true
trace_id:
type: string
PaginationMeta:
type: object
required: [page, limit, total, total_pages]
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
total_pages: { type: integer }
has_next: { type: boolean }
has_prev: { type: boolean }undefinedShared Utilities
共享工具类
typescript
// utils/api-response.ts
export class ApiResponseBuilder {
static success<T>(data: T, meta?: Partial<ResponseMeta>): ApiResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
...meta,
},
};
}
static paginated<T>(
data: T[],
pagination: PaginationMeta
): PaginatedResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
...pagination,
},
};
}
static error(code: ErrorCode, message: string, details?: any): ApiResponse {
return {
success: false,
error: { code, message, details },
meta: {
timestamp: new Date().toISOString(),
},
};
}
}typescript
// utils/api-response.ts
export class ApiResponseBuilder {
static success<T>(data: T, meta?: Partial<ResponseMeta>): ApiResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
...meta,
},
};
}
static paginated<T>(
data: T[],
pagination: PaginationMeta
): PaginatedResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
...pagination,
},
};
}
static error(code: ErrorCode, message: string, details?: any): ApiResponse {
return {
success: false,
error: { code, message, details },
meta: {
timestamp: new Date().toISOString(),
},
};
}
}Best Practices
最佳实践
- Consistent envelope: All responses use same structure
- Type safety: Shared types across frontend/backend
- Clear errors: Descriptive codes and messages
- Standard pagination: Same format for all lists
- Versioning: Plan for API evolution
- Documentation: OpenAPI spec as source of truth
- Gradual migration: Don't break existing clients
- Monitoring: Track adoption and errors
- 统一响应信封:所有接口使用相同的响应结构
- 类型安全:前后端共享类型定义
- 清晰的错误信息:提供描述性的错误码和消息
- 标准分页格式:所有列表接口使用统一分页规则
- 版本控制规划:为API演进做好准备
- 文档作为唯一可信源:以OpenAPI规范为基准
- 渐进式迁移:避免影响现有客户端
- 监控跟踪:跟踪用户采用情况和错误日志
Output Checklist
输出检查清单
- Standard response envelope defined
- Error taxonomy documented
- Pagination format standardized
- Status code mapping
- Normalization middleware
- Shared TypeScript types
- Versioning strategy
- OpenAPI specification
- Migration plan with phases
- Consumer communication plan
- 定义标准响应信封
- 文档化错误分类
- 标准化分页格式
- 状态码映射关系
- 响应归一化中间件
- 共享TypeScript类型
- 版本控制策略
- OpenAPI规范文档
- 分阶段迁移计划
- 消费者沟通方案