mcpField
A
api-contract-normalizer

api-contract-normalizer

github.com/patricio0312rev開発63280 回インストール41 週間前に更新
リポジトリ

api-contract-normalizer について

API エンドポイントの契約形式を統一するためのスキルです。レスポンス、エラー、ページネーション、バージョン管理の規約を、TypeScript 型、ミドルウェア、OpenAPI ドキュメント、移行計画とともに整理します。

主な機能

  1. 01共通レスポンス形式の定義
  2. 02ページネーションとエラーの標準化
  3. 03共通 TypeScript 型の作成
  4. 04ミドルウェアによるレスポンス統一
  5. 05OpenAPI による契約の文書化
  6. 06バージョン管理と移行の計画

活用シーン

  1. 01エンドポイント間のレスポンス形式の統一
  2. 02利用者向け API 契約の文書化
  3. 03契約変更の段階的な導入

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patricio0312rev/skills --skill api-contract-normalizer

インストール後、Claude Code で/api-contract-normalizerを入力して読み込まれたか確認します。

詳細

API コントラクトの標準化

一貫性と開発者エクスペリエンスのために、すべてのエンドポイントで API コントラクトを標準化します。

コアワークフロー

  1. 既存 API の監査:現在の不整合を文書化します
  2. 標準の定義:レスポンス形式、ページネーション、エラー、ステータスコードを定義します
  3. 共有型の作成:すべてのコントラクト用の TypeScript インターフェースを作成します
  4. ミドルウェアの構築:レスポンスを自動的に標準化します
  5. コントラクトの文書化:例を含む OpenAPI 仕様を作成します
  6. 移行計画:段階的な導入戦略を策定します
  7. バージョニング:API のバージョニング戦略を策定します

標準レスポンスエンベロープ

// 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;
}

ページネーションの標準

// 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;
}

エラーの標準

// 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"
  }
}

レスポンス標準化ミドルウェア

// 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",
      },
    });
  };
}

ステータスコードの標準

// 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
};

バージョニング戦略

// 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);

移行戦略

# API Contract Migration Plan

## Phase 1: Add Normalization (Week 1-2)

- [ ] Deploy normalization middleware
- [ ] Run alongside existing responses
- [ ] Monitor for issues
- [ ] No breaking changes yet

## Phase 2: Deprecation Notice (Week 3-4)

- [ ] Add deprecation headers
- [ ] Update documentation
- [ ] Notify API consumers
- [ ] Provide migration guide

## Phase 3: Dual Format Support (Week 5-8)

- [ ] Support both old and new formats
- [ ] Add ?format=v2 query param
- [ ] Track adoption metrics
- [ ] Help consumers migrate

## Phase 4: Switch Default (Week 9-10)

- [ ] New format becomes default
- [ ] Old format requires ?format=v1
- [ ] Final migration reminders
- [ ] Extended support period

## Phase 5: Remove Old Format (Week 12+)

- [ ] Remove old format support
- [ ] Clean up legacy code
- [ ] Update all documentation
- [ ] Celebrate consistency! 🎉

コントラクトのドキュメント

# 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 }

共有ユーティリティ

// 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(),
      },
    };
  }
}

ベストプラクティス

  1. 一貫したエンベロープ:すべてのレスポンスで同じ構造を使用します
  2. 型安全性:フロントエンドとバックエンドで共有型を使用します
  3. 明確なエラー:説明的なコードとメッセージを使用します
  4. 標準ページネーション:すべてのリストで同じ形式を使用します
  5. バージョニング:API の進化に備えて計画します
  6. ドキュメント:信頼できる情報源として OpenAPI 仕様を使用します
  7. 段階的な移行:既存のクライアントに影響を与えないようにします
  8. モニタリング:導入状況とエラーを追跡します

出力チェックリスト

  • 標準レスポンスエンベロープを定義
  • エラー分類を文書化
  • ページネーション形式を標準化
  • ステータスコードのマッピング
  • 標準化ミドルウェア
  • 共有 TypeScript 型
  • バージョニング戦略
  • OpenAPI 仕様
  • フェーズを含む移行計画
  • 利用者へのコミュニケーション計画

コメント