api-contract-normalizer について
API エンドポイントの契約形式を統一するためのスキルです。レスポンス、エラー、ページネーション、バージョン管理の規約を、TypeScript 型、ミドルウェア、OpenAPI ドキュメント、移行計画とともに整理します。
主な機能
- 01共通レスポンス形式の定義
- 02ページネーションとエラーの標準化
- 03共通 TypeScript 型の作成
- 04ミドルウェアによるレスポンス統一
- 05OpenAPI による契約の文書化
- 06バージョン管理と移行の計画
活用シーン
- 01エンドポイント間のレスポンス形式の統一
- 02利用者向け API 契約の文書化
- 03契約変更の段階的な導入
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill api-contract-normalizerインストール後、Claude Code で/api-contract-normalizerを入力して読み込まれたか確認します。
詳細
API コントラクトの標準化
一貫性と開発者エクスペリエンスのために、すべてのエンドポイントで API コントラクトを標準化します。
コアワークフロー
- 既存 API の監査:現在の不整合を文書化します
- 標準の定義:レスポンス形式、ページネーション、エラー、ステータスコードを定義します
- 共有型の作成:すべてのコントラクト用の TypeScript インターフェースを作成します
- ミドルウェアの構築:レスポンスを自動的に標準化します
- コントラクトの文書化:例を含む OpenAPI 仕様を作成します
- 移行計画:段階的な導入戦略を策定します
- バージョニング: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(),
},
};
}
}
ベストプラクティス
- 一貫したエンベロープ:すべてのレスポンスで同じ構造を使用します
- 型安全性:フロントエンドとバックエンドで共有型を使用します
- 明確なエラー:説明的なコードとメッセージを使用します
- 標準ページネーション:すべてのリストで同じ形式を使用します
- バージョニング:API の進化に備えて計画します
- ドキュメント:信頼できる情報源として OpenAPI 仕様を使用します
- 段階的な移行:既存のクライアントに影響を与えないようにします
- モニタリング:導入状況とエラーを追跡します
出力チェックリスト
- 標準レスポンスエンベロープを定義
- エラー分類を文書化
- ページネーション形式を標準化
- ステータスコードのマッピング
- 標準化ミドルウェア
- 共有 TypeScript 型
- バージョニング戦略
- OpenAPI 仕様
- フェーズを含む移行計画
- 利用者へのコミュニケーション計画
コメント
Markdown 対応