api-contract-normalizer
SKILL.md
API Contract Normalizer
Standardize API contracts across all endpoints for consistency and developer experience.
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
Standard Response Envelope
// 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
// 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 Standards
// 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"
}
}
Response Normalization Middleware
// 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",
},
});
};
}
Status Code Standards
// 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
};
Versioning Strategy
// 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);
Migration Strategy
# 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! 🎉
Contract Documentation
# 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 }
Shared Utilities
// 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
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
Weekly Installs
10
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code8
gemini-cli7
antigravity7
windsurf7
github-copilot7
codex7