api-contract-normalizer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Contract Normalizer

API 契约标准化工具

Standardize API contracts across all endpoints for consistency and developer experience.
统一所有接口的API契约,提升一致性与开发者体验。

Core Workflow

核心工作流程

  1. Audit existing APIs: Document current inconsistencies
  2. Define standards: Response format, pagination, errors, status codes
  3. Create shared types: TypeScript interfaces for all contracts
  4. Build middleware: Normalize responses automatically
  5. Document contract: OpenAPI spec with examples
  6. Migration plan: Phased rollout strategy
  7. Versioning: API version strategy
  1. 审计现有API:记录当前存在的不一致问题
  2. 定义标准:响应格式、分页规则、错误结构、状态码
  3. 创建共享类型:为所有契约定义TypeScript接口
  4. 构建中间件:自动归一化响应内容
  5. 文档化契约:包含示例的OpenAPI规范
  6. 迁移计划:分阶段落地策略
  7. 版本控制: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
undefined
markdown
undefined

API 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一致性达成!🎉
undefined

Contract Documentation

契约文档

yaml
undefined
yaml
undefined

openapi.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 }
undefined
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 }
undefined

Shared 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

最佳实践

  1. Consistent envelope: All responses use same structure
  2. Type safety: Shared types across frontend/backend
  3. Clear errors: Descriptive codes and messages
  4. Standard pagination: Same format for all lists
  5. Versioning: Plan for API evolution
  6. Documentation: OpenAPI spec as source of truth
  7. Gradual migration: Don't break existing clients
  8. Monitoring: Track adoption and errors
  1. 统一响应信封:所有接口使用相同的响应结构
  2. 类型安全:前后端共享类型定义
  3. 清晰的错误信息:提供描述性的错误码和消息
  4. 标准分页格式:所有列表接口使用统一分页规则
  5. 版本控制规划:为API演进做好准备
  6. 文档作为唯一可信源:以OpenAPI规范为基准
  7. 渐进式迁移:避免影响现有客户端
  8. 监控跟踪:跟踪用户采用情况和错误日志

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规范文档
  • 分阶段迁移计划
  • 消费者沟通方案