API Design Patterns RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation. Metadata - Version: 2.0.0 - Rule Count: 38 rules across 7 categories - License: MIT When to Apply Reference these guidelines when: - Designing new API endpoints - Reviewing existing API structure - Implementing error handling and validation - Setting up pagination, filtering, and sorting - Planning API versioning strategy - Configuring API securit…

)\n age: Optional[int] = Field(None, ge=0, le=150)\n\n @validator('password')\n def password_complexity(cls, v):\n errors = []\n if not any(c.isupper() for c in v):\n errors.append('must contain an uppercase letter')\n if not any(c.islower() for c in v):\n errors.append('must contain a lowercase letter')\n if not any(c.isdigit() for c in v):\n errors.append('must contain a number')\n if errors:\n raise ValueError(f\"Password {', '.join(errors)}\")\n return v\n\[email protected]_handler(RequestValidationError)\nasync def validation_exception_handler(request: Request, exc: RequestValidationError):\n details = []\n for error in exc.errors():\n field = '.'.join(str(loc) for loc in error['loc'] if loc != 'body')\n details.append({\n 'field': field,\n 'message': error['msg'],\n 'type': error['type'],\n 'context': error.get('ctx', {})\n })\n\n return JSONResponse(\n status_code=422,\n content={\n 'error': {\n 'code': 'validation_error',\n 'message': f'{len(details)} validation error(s) found',\n 'details': details\n }\n }\n )\n\[email protected](\"/users\")\nasync def create_user(user: UserCreate):\n return {\"id\": 1, **user.dict()}\n```\n\n```typescript\n// ✅ TypeScript/Zod validation with detailed errors\nimport { z } from 'zod';\nimport express from 'express';\n\nconst UserSchema = z.object({\n email: z.string()\n .min(1, 'Email is required')\n .email('Must be a valid email address'),\n\n password: z.string()\n .min(8, 'Password must be at least 8 characters')\n .regex(/[A-Z]/, 'Password must contain an uppercase letter')\n .regex(/[a-z]/, 'Password must contain a lowercase letter')\n .regex(/[0-9]/, 'Password must contain a number'),\n\n username: z.string()\n .min(3, 'Username must be at least 3 characters')\n .max(30, 'Username cannot exceed 30 characters')\n .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),\n\n age: z.number()\n .int('Age must be a whole number')\n .min(0, 'Age cannot be negative')\n .max(150, 'Age cannot exceed 150')\n .optional()\n});\n\napp.post('/users', (req, res) => {\n const result = UserSchema.safeParse(req.body);\n\n if (!result.success) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Validation failed',\n details: result.error.errors.map(err => ({\n field: err.path.join('.'),\n message: err.message,\n code: err.code\n }))\n }\n });\n }\n\n // Create user with result.data\n});\n```\n\n## Nested Object Validation\n\n```json\n// ✅ Validation errors for nested objects\n{\n \"error\": {\n \"code\": \"validation_error\",\n \"message\": \"Validation failed\",\n \"details\": [\n {\n \"field\": \"address.zipCode\",\n \"message\": \"ZIP code must be 5 digits\",\n \"value\": \"123\"\n },\n {\n \"field\": \"address.country\",\n \"message\": \"Country is required\"\n },\n {\n \"field\": \"contacts[0].email\",\n \"message\": \"Invalid email format\",\n \"value\": \"bad-email\"\n },\n {\n \"field\": \"contacts[1].phone\",\n \"message\": \"Phone number must include country code\",\n \"value\": \"555-1234\"\n }\n ]\n }\n}\n```\n\n**Benefits:**\n- Clients can highlight specific form fields with their errors\n- Users see exactly which fields need attention without guessing\n- Developers can quickly identify validation issues during development\n- Users can fix all issues at once instead of submitting multiple times\n- Frontend validation libraries can map errors directly to form fields\n- Clear validation responses help document expected input formats\n\nReference: [JSON:API Error Objects](https://jsonapi.org/format/#error-objects)\n\n\n---\n\n## Use Machine-Readable Error Codes\n\n**Impact: CRITICAL (Enables programmatic error handling and client recovery)**\n\nInclude standardized, machine-readable error codes alongside human-readable messages to enable programmatic error handling.\n\n## Incorrect\n\n```json\n// ❌ Only human-readable messages\n{\n \"error\": \"The user was not found\"\n}\n\n// ❌ HTTP status codes only\n{\n \"status\": 404\n}\n\n// ❌ Inconsistent or vague codes\n{\n \"error_code\": \"ERR001\"\n}\n\n{\n \"code\": 1234\n}\n\n{\n \"error_type\": \"bad_thing_happened\"\n}\n```\n\n```javascript\n// ❌ No error codes for programmatic handling\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n if (!user) {\n // Client can't reliably detect \"not found\" vs other 404s\n res.status(404).json({ message: 'User not found' });\n }\n});\n```\n\n**Problems:**\n- Clients must parse human-readable messages which change with localization\n- No stable identifier for programmatic error handling\n- Numeric codes are meaningless without a reference table\n- Inconsistent code formats across endpoints\n- Cannot implement targeted recovery strategies per error type\n- Monitoring dashboards cannot categorize errors precisely\n\n## Correct\n\n```javascript\n// ✅ Define error codes as constants\nconst ErrorCodes = {\n // Authentication & Authorization\n AUTH_TOKEN_MISSING: 'auth_token_missing',\n AUTH_TOKEN_INVALID: 'auth_token_invalid',\n AUTH_TOKEN_EXPIRED: 'auth_token_expired',\n AUTH_INSUFFICIENT_PERMISSIONS: 'auth_insufficient_permissions',\n\n // Validation\n VALIDATION_ERROR: 'validation_error',\n VALIDATION_REQUIRED_FIELD: 'validation_required_field',\n VALIDATION_INVALID_FORMAT: 'validation_invalid_format',\n VALIDATION_OUT_OF_RANGE: 'validation_out_of_range',\n\n // Resources\n RESOURCE_NOT_FOUND: 'resource_not_found',\n RESOURCE_ALREADY_EXISTS: 'resource_already_exists',\n RESOURCE_CONFLICT: 'resource_conflict',\n RESOURCE_DELETED: 'resource_deleted',\n\n // Rate Limiting\n RATE_LIMIT_EXCEEDED: 'rate_limit_exceeded',\n\n // Business Logic\n INSUFFICIENT_FUNDS: 'insufficient_funds',\n INVENTORY_UNAVAILABLE: 'inventory_unavailable',\n ORDER_CANNOT_BE_CANCELLED: 'order_cannot_be_cancelled',\n SUBSCRIPTION_EXPIRED: 'subscription_expired',\n\n // Server Errors\n INTERNAL_ERROR: 'internal_error',\n SERVICE_UNAVAILABLE: 'service_unavailable',\n DEPENDENCY_ERROR: 'dependency_error'\n};\n\n// Error factory\nclass APIError extends Error {\n constructor(code, message, statusCode = 400, details = null) {\n super(message);\n this.code = code;\n this.statusCode = statusCode;\n this.details = details;\n }\n}\n\n// Usage in routes\napp.get('/users/:id', async (req, res, next) => {\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n throw new APIError(\n ErrorCodes.RESOURCE_NOT_FOUND,\n 'User not found',\n 404,\n { resourceType: 'user', resourceId: req.params.id }\n );\n }\n res.json(user);\n } catch (error) {\n next(error);\n }\n});\n\napp.post('/orders', async (req, res, next) => {\n try {\n const product = await db.findProduct(req.body.productId);\n\n if (product.stock \u003c req.body.quantity) {\n throw new APIError(\n ErrorCodes.INVENTORY_UNAVAILABLE,\n 'Not enough items in stock',\n 422,\n {\n requested: req.body.quantity,\n available: product.stock,\n productId: req.body.productId\n }\n );\n }\n\n // Process order...\n } catch (error) {\n next(error);\n }\n});\n\n// Error handler\napp.use((err, req, res, next) => {\n if (err instanceof APIError) {\n return res.status(err.statusCode).json({\n error: {\n code: err.code,\n message: err.message,\n details: err.details\n }\n });\n }\n\n // Unknown error\n res.status(500).json({\n error: {\n code: ErrorCodes.INTERNAL_ERROR,\n message: 'An unexpected error occurred'\n }\n });\n});\n```\n\n```python\n# ✅ Python with error codes\nfrom enum import Enum\nfrom fastapi import FastAPI, HTTPException\nfrom typing import Optional, Any\n\nclass ErrorCode(str, Enum):\n # Authentication\n AUTH_TOKEN_MISSING = \"auth_token_missing\"\n AUTH_TOKEN_INVALID = \"auth_token_invalid\"\n AUTH_TOKEN_EXPIRED = \"auth_token_expired\"\n AUTH_INSUFFICIENT_PERMISSIONS = \"auth_insufficient_permissions\"\n\n # Validation\n VALIDATION_ERROR = \"validation_error\"\n VALIDATION_REQUIRED_FIELD = \"validation_required_field\"\n\n # Resources\n RESOURCE_NOT_FOUND = \"resource_not_found\"\n RESOURCE_ALREADY_EXISTS = \"resource_already_exists\"\n RESOURCE_CONFLICT = \"resource_conflict\"\n\n # Business Logic\n INSUFFICIENT_FUNDS = \"insufficient_funds\"\n INVENTORY_UNAVAILABLE = \"inventory_unavailable\"\n\n # Server\n INTERNAL_ERROR = \"internal_error\"\n SERVICE_UNAVAILABLE = \"service_unavailable\"\n\nclass APIError(Exception):\n def __init__(\n self,\n code: ErrorCode,\n message: str,\n status_code: int = 400,\n details: Optional[dict] = None\n ):\n self.code = code\n self.message = message\n self.status_code = status_code\n self.details = details\n\napp = FastAPI()\n\[email protected]_handler(APIError)\nasync def api_error_handler(request, exc: APIError):\n return JSONResponse(\n status_code=exc.status_code,\n content={\n \"error\": {\n \"code\": exc.code.value,\n \"message\": exc.message,\n \"details\": exc.details\n }\n }\n )\n\[email protected](\"/users/{user_id}\")\nasync def get_user(user_id: int):\n user = await db.get_user(user_id)\n if not user:\n raise APIError(\n code=ErrorCode.RESOURCE_NOT_FOUND,\n message=f\"User with ID {user_id} not found\",\n status_code=404,\n details={\"resource_type\": \"user\", \"resource_id\": user_id}\n )\n return user\n```\n\n```json\n// ✅ Error response with code\n{\n \"error\": {\n \"code\": \"inventory_unavailable\",\n \"message\": \"Not enough items in stock to fulfill your order\",\n \"details\": {\n \"productId\": \"prod_123\",\n \"productName\": \"Widget Pro\",\n \"requested\": 10,\n \"available\": 3\n }\n }\n}\n```\n\n```typescript\n// ✅ Client-side error handling\nasync function createOrder(orderData: OrderData): Promise\u003cOrder> {\n const response = await fetch('/api/orders', {\n method: 'POST',\n body: JSON.stringify(orderData)\n });\n\n if (!response.ok) {\n const error = await response.json();\n\n // Programmatic handling based on error code\n switch (error.error.code) {\n case 'inventory_unavailable':\n showInventoryWarning(error.error.details);\n break;\n\n case 'insufficient_funds':\n redirectToPaymentUpdate();\n break;\n\n case 'auth_token_expired':\n await refreshToken();\n return createOrder(orderData); // Retry\n\n case 'rate_limit_exceeded':\n await delay(error.error.details.retryAfter * 1000);\n return createOrder(orderData); // Retry\n\n default:\n showGenericError(error.error.message);\n }\n\n throw new APIError(error);\n }\n\n return response.json();\n}\n```\n\n## Error Code Naming Conventions\n\n| Category | Pattern | Examples |\n|----------|---------|----------|\n| Auth | `auth_*` | `auth_token_expired`, `auth_invalid_credentials` |\n| Validation | `validation_*` | `validation_error`, `validation_invalid_email` |\n| Resource | `resource_*` | `resource_not_found`, `resource_conflict` |\n| Business | Domain-specific | `insufficient_funds`, `inventory_unavailable` |\n| Rate Limit | `rate_limit_*` | `rate_limit_exceeded` |\n| Server | `internal_*` or `service_*` | `internal_error`, `service_unavailable` |\n\n**Benefits:**\n- Code can switch on error codes to take appropriate recovery action\n- Error codes remain stable even when messages change or are localized\n- Error codes can be documented and referenced in API docs\n- Precise alerting and monitoring dashboards based on error categories\n- Tests can assert on specific error codes rather than message strings\n- Messages can be translated while codes stay constant across locales\n\nReference: [Google Cloud API Error Model](https://cloud.google.com/apis/design/errors)\n\n\n---\n\n## Never Expose Stack Traces in Production\n\n**Impact: CRITICAL (Prevents security vulnerabilities and information disclosure)**\n\nStack traces and internal error details should never be exposed to API clients in production environments, as they reveal implementation details and potential vulnerabilities.\n\n## Incorrect\n\n```json\n// ❌ Full stack trace in production response\n{\n \"error\": \"Cannot read property 'id' of undefined\",\n \"stack\": \"TypeError: Cannot read property 'id' of undefined\\n at getUserOrders (/app/src/controllers/orders.js:45:23)\\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\\n at next (/app/node_modules/express/lib/router/route.js:137:13)\\n at authenticate (/app/src/middleware/auth.js:28:5)\\n at /app/node_modules/express/lib/router/index.js:284:15\"\n}\n\n// ❌ Database error details exposed\n{\n \"error\": \"SequelizeConnectionError: Connection refused to host 'db.internal.company.com' port 5432\",\n \"sql\": \"SELECT * FROM users WHERE id = 1 AND deleted_at IS NULL\"\n}\n\n// ❌ Internal paths and configuration\n{\n \"error\": \"ENOENT: no such file or directory, open '/var/app/config/secrets.json'\"\n}\n```\n\n```javascript\n// ❌ Exposing all error details\napp.use((err, req, res, next) => {\n res.status(500).json({\n error: err.message,\n stack: err.stack, // Never do this in production!\n code: err.code\n });\n});\n```\n\n**Problems:**\n- Stack traces reveal file paths, dependencies, and code structure attackers can exploit\n- Database error messages may expose schemas, connection strings, or query logic\n- Internal file paths reveal server configuration and directory structure\n- Framework and version information helps attackers find known vulnerabilities\n- Violates security standards like PCI-DSS and SOC 2\n\n## Correct\n\n```javascript\n// ✅ Secure error handler\nconst isProduction = process.env.NODE_ENV === 'production';\n\napp.use((err, req, res, next) => {\n // Log full error internally\n logger.error('Request error', {\n error: err.message,\n stack: err.stack,\n requestId: req.id,\n path: req.path,\n method: req.method,\n userId: req.user?.id\n });\n\n // Determine if error is safe to expose\n const isOperationalError = err.isOperational || err.expose;\n const statusCode = err.statusCode || 500;\n\n // Build safe response\n const errorResponse = {\n error: {\n code: err.code || 'internal_error',\n message: isOperationalError\n ? err.message\n : 'An unexpected error occurred. Please try again later.',\n requestId: req.id\n }\n };\n\n // Only include details in development\n if (!isProduction && err.stack) {\n errorResponse.error._debug = {\n message: err.message,\n stack: err.stack.split('\\n')\n };\n }\n\n res.status(statusCode).json(errorResponse);\n});\n\n// Custom error class for operational errors\nclass APIError extends Error {\n constructor(message, statusCode = 500, code = 'internal_error') {\n super(message);\n this.statusCode = statusCode;\n this.code = code;\n this.isOperational = true; // Safe to expose\n }\n}\n\n// Database error handling\napp.get('/users/:id', async (req, res, next) => {\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n throw new APIError('User not found', 404, 'resource_not_found');\n }\n res.json(user);\n } catch (error) {\n if (error instanceof APIError) {\n return next(error);\n }\n\n // Log the actual database error\n logger.error('Database error', {\n error: error.message,\n stack: error.stack,\n query: 'findUser',\n params: { id: req.params.id }\n });\n\n // Return generic error to client\n next(new APIError(\n 'Unable to retrieve user information',\n 500,\n 'service_error'\n ));\n }\n});\n```\n\n```python\n# ✅ FastAPI with secure error handling\nimport logging\nimport traceback\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\nimport os\n\napp = FastAPI()\nlogger = logging.getLogger(__name__)\nIS_PRODUCTION = os.getenv(\"ENVIRONMENT\") == \"production\"\n\nclass APIError(Exception):\n def __init__(self, message: str, status_code: int = 500, code: str = \"internal_error\"):\n self.message = message\n self.status_code = status_code\n self.code = code\n self.is_operational = True\n\[email protected]_handler(APIError)\nasync def api_error_handler(request: Request, exc: APIError):\n return JSONResponse(\n status_code=exc.status_code,\n content={\n \"error\": {\n \"code\": exc.code,\n \"message\": exc.message,\n \"request_id\": request.state.request_id\n }\n }\n )\n\[email protected]_handler(Exception)\nasync def generic_error_handler(request: Request, exc: Exception):\n # Log full error internally\n logger.error(\n \"Unhandled exception\",\n extra={\n \"error\": str(exc),\n \"traceback\": traceback.format_exc(),\n \"request_id\": request.state.request_id,\n \"path\": request.url.path,\n \"method\": request.method\n }\n )\n\n # Return safe response\n content = {\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"request_id\": request.state.request_id\n }\n }\n\n # Include debug info only in development\n if not IS_PRODUCTION:\n content[\"error\"][\"_debug\"] = {\n \"type\": type(exc).__name__,\n \"message\": str(exc),\n \"traceback\": traceback.format_exc().split(\"\\n\")\n }\n\n return JSONResponse(status_code=500, content=content)\n```\n\n```json\n// ✅ Production error response (safe)\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"requestId\": \"req-abc123\"\n }\n}\n\n// ✅ Development error response (with debug info)\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"requestId\": \"req-abc123\",\n \"_debug\": {\n \"type\": \"TypeError\",\n \"message\": \"Cannot read property 'id' of undefined\",\n \"traceback\": [\n \"Traceback (most recent call last):\",\n \" File \\\"app.py\\\", line 45, in get_user\",\n \" return user.id\",\n \"TypeError: Cannot read property 'id' of undefined\"\n ]\n }\n }\n}\n```\n\n## What to Log vs. What to Return\n\n| Information | Log Internally | Return to Client |\n|-------------|----------------|------------------|\n| Error message | Yes | Generic only |\n| Stack trace | Yes | Never in production |\n| SQL queries | Yes | Never |\n| File paths | Yes | Never |\n| Internal IPs | Yes | Never |\n| Request ID | Yes | Yes |\n| Error code | Yes | Yes |\n| User ID | Yes | No |\n| Timestamps | Yes | Optional |\n\n**Benefits:**\n- Prevents attackers from exploiting revealed file paths, dependencies, and code structure\n- Protects database schemas, API keys, and other secrets from leaking\n- Hides framework versions that could expose known vulnerabilities\n- Clean error messages present a professional API to consumers\n- Meets compliance requirements for PCI-DSS, SOC 2, and similar standards\n- Request IDs enable correlation between client reports and internal logs\n\nReference: [OWASP Improper Error Handling](https://owasp.org/www-community/Improper_Error_Handling)\n\n\n---\n\n## Include Request ID in Error Responses\n\n**Impact: CRITICAL (Enables log correlation and efficient debugging)**\n\nEvery API request should have a unique identifier that appears in both the response and server logs, enabling easy correlation for debugging.\n\n## Incorrect\n\n```json\n// ❌ No request identifier\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred\"\n }\n}\n// User reports error, but support can't find it in logs\n```\n\n```javascript\n// ❌ No request tracking\napp.get('/users/:id', async (req, res) => {\n try {\n const user = await db.findUser(req.params.id);\n res.json(user);\n } catch (error) {\n console.log('Error:', error.message); // No way to correlate\n res.status(500).json({ error: 'Something went wrong' });\n }\n});\n```\n\n**Problems:**\n- Support cannot locate specific errors in logs when users report issues\n- No way to correlate client-side errors with server-side logs\n- Debugging requires time-based log searching which is imprecise\n- Distributed systems cannot trace requests across services\n- No audit trail for specific request flows\n\n## Correct\n\n```javascript\n// ✅ Request ID middleware\nconst { v4: uuidv4 } = require('uuid');\n\napp.use((req, res, next) => {\n // Use client-provided ID or generate new one\n req.id = req.headers['x-request-id'] || uuidv4();\n\n // Add to response headers\n res.setHeader('X-Request-ID', req.id);\n\n // Add to logger context\n req.log = logger.child({ requestId: req.id });\n\n next();\n});\n\n// Use in routes\napp.get('/users/:id', async (req, res, next) => {\n req.log.info('Fetching user', { userId: req.params.id });\n\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n return res.status(404).json({\n error: {\n code: 'resource_not_found',\n message: 'User not found',\n requestId: req.id\n }\n });\n }\n res.json(user);\n } catch (error) {\n req.log.error('Failed to fetch user', {\n error: error.message,\n stack: error.stack\n });\n next(error);\n }\n});\n\n// Error handler includes request ID\napp.use((err, req, res, next) => {\n req.log.error('Request failed', {\n error: err.message,\n stack: err.stack,\n statusCode: err.statusCode || 500\n });\n\n res.status(err.statusCode || 500).json({\n error: {\n code: err.code || 'internal_error',\n message: err.message || 'An unexpected error occurred',\n requestId: req.id\n }\n });\n});\n```\n\n```python\n# ✅ FastAPI with request ID\nimport uuid\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\nimport logging\nfrom contextvars import ContextVar\n\napp = FastAPI()\nlogger = logging.getLogger(__name__)\n\n# Context variable for request ID\nrequest_id_var: ContextVar[str] = ContextVar(\"request_id\", default=\"\")\n\[email protected](\"http\")\nasync def request_id_middleware(request: Request, call_next):\n # Get or generate request ID\n request_id = request.headers.get(\"X-Request-ID\", str(uuid.uuid4()))\n request.state.request_id = request_id\n request_id_var.set(request_id)\n\n # Process request\n response = await call_next(request)\n\n # Add request ID to response\n response.headers[\"X-Request-ID\"] = request_id\n return response\n\n# Custom log filter to include request ID\nclass RequestIdFilter(logging.Filter):\n def filter(self, record):\n record.request_id = request_id_var.get(\"\")\n return True\n\n# Configure logging\nhandler = logging.StreamHandler()\nhandler.addFilter(RequestIdFilter())\nhandler.setFormatter(logging.Formatter(\n '%(asctime)s [%(request_id)s] %(levelname)s: %(message)s'\n))\nlogger.addHandler(handler)\n\[email protected]_handler(Exception)\nasync def error_handler(request: Request, exc: Exception):\n logger.error(f\"Request failed: {exc}\", exc_info=True)\n\n return JSONResponse(\n status_code=500,\n content={\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred\",\n \"requestId\": request.state.request_id\n }\n },\n headers={\"X-Request-ID\": request.state.request_id}\n )\n\[email protected](\"/users/{user_id}\")\nasync def get_user(user_id: int, request: Request):\n logger.info(f\"Fetching user {user_id}\")\n\n user = await db.get_user(user_id)\n if not user:\n return JSONResponse(\n status_code=404,\n content={\n \"error\": {\n \"code\": \"resource_not_found\",\n \"message\": f\"User {user_id} not found\",\n \"requestId\": request.state.request_id\n }\n }\n )\n return user\n```\n\n```json\n// ✅ Error response with request ID\nHTTP/1.1 500 Internal Server Error\nX-Request-ID: req-550e8400-e29b-41d4-a716-446655440000\n\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again.\",\n \"requestId\": \"req-550e8400-e29b-41d4-a716-446655440000\"\n }\n}\n```\n\n```bash\n# ✅ Server logs with request ID\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] INFO: Fetching user 123\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] ERROR: Database connection timeout\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] ERROR: Request failed\n```\n\n## Distributed Tracing Integration\n\n```javascript\n// ✅ Integration with OpenTelemetry\nconst { trace, context } = require('@opentelemetry/api');\n\napp.use((req, res, next) => {\n const span = trace.getActiveSpan();\n\n // Use trace ID as request ID for distributed tracing\n if (span) {\n const traceId = span.spanContext().traceId;\n req.id = traceId;\n req.spanContext = span.spanContext();\n } else {\n req.id = uuidv4();\n }\n\n res.setHeader('X-Request-ID', req.id);\n next();\n});\n```\n\n```yaml\n# ✅ OpenAPI documentation for request ID\ncomponents:\n headers:\n X-Request-ID:\n description: Unique identifier for the request, used for debugging and log correlation\n schema:\n type: string\n format: uuid\n example: \"550e8400-e29b-41d4-a716-446655440000\"\n\n schemas:\n Error:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n message:\n type: string\n requestId:\n type: string\n description: Unique request identifier for support correlation\n```\n\n**Benefits:**\n- Users can provide the request ID when reporting issues for instant log lookup\n- Links all log entries for a single request across multiple services\n- Request IDs propagate through microservices for end-to-end distributed tracing\n- \"Please provide the request ID\" is faster than \"describe what you did\"\n- Enables tracking individual request paths through infrastructure\n- Audit trails require the ability to trace specific requests\n\nReference: [OpenTelemetry Tracing](https://opentelemetry.io/docs/concepts/signals/traces/)\n\n\n---\n\n## Implement Secure Authentication\n\n**Impact: CRITICAL (Protects user data and prevents unauthorized access)**\n\nUse industry-standard authentication mechanisms like OAuth 2.0, JWT, or API keys with proper security practices.\n\n## Incorrect\n\n```javascript\n// ❌ Basic auth over HTTP\napp.use((req, res, next) => {\n const auth = req.headers.authorization;\n const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64')\n .toString().split(':');\n // Credentials sent in plain text!\n if (user === 'admin' && pass === 'password123') {\n next();\n }\n});\n\n// ❌ Token in URL\napp.get('/users?token=secret123', (req, res) => {\n // Token visible in logs, browser history, referrer headers\n});\n\n// ❌ No token expiration\nconst token = jwt.sign({ userId: 123 }); // No expiration!\n\n// ❌ Weak secret\nconst token = jwt.sign({ userId: 123 }, 'secret'); // Easily guessable\n```\n\n```json\n// ❌ Credentials in response body\n{\n \"user\": {\n \"id\": 123,\n \"password\": \"hashedpassword\",\n \"apiKey\": \"sk_live_abc123\"\n }\n}\n```\n\n**Problems:**\n- Plain text credentials can be intercepted over HTTP\n- Tokens in URLs are logged by servers, proxies, and browsers\n- Non-expiring tokens remain valid forever if compromised\n- Weak secrets allow token forgery through brute force\n- Exposing credentials in responses enables account takeover\n\n## Correct\n\n```javascript\n// ✅ Secure JWT authentication\nconst jwt = require('jsonwebtoken');\nconst bcrypt = require('bcrypt');\n\n// Environment-based secrets\nconst JWT_SECRET = process.env.JWT_SECRET; // Long, random string\nconst JWT_EXPIRES_IN = '15m'; // Short-lived access tokens\nconst REFRESH_EXPIRES_IN = '7d';\n\n// Password hashing\nasync function hashPassword(password) {\n const saltRounds = 12;\n return bcrypt.hash(password, saltRounds);\n}\n\nasync function verifyPassword(password, hash) {\n return bcrypt.compare(password, hash);\n}\n\n// JWT token generation\nfunction generateTokens(user) {\n const accessToken = jwt.sign(\n {\n sub: user.id,\n email: user.email,\n roles: user.roles\n },\n JWT_SECRET,\n {\n expiresIn: JWT_EXPIRES_IN,\n issuer: 'api.example.com',\n audience: 'example.com'\n }\n );\n\n const refreshToken = jwt.sign(\n { sub: user.id, type: 'refresh' },\n JWT_SECRET,\n { expiresIn: REFRESH_EXPIRES_IN }\n );\n\n return { accessToken, refreshToken };\n}\n\n// Authentication endpoint\napp.post('/auth/login', async (req, res) => {\n const { email, password } = req.body;\n\n const user = await db.findUserByEmail(email);\n if (!user) {\n // Don't reveal if user exists\n return res.status(401).json({\n error: {\n code: 'invalid_credentials',\n message: 'Invalid email or password'\n }\n });\n }\n\n const valid = await verifyPassword(password, user.passwordHash);\n if (!valid) {\n return res.status(401).json({\n error: {\n code: 'invalid_credentials',\n message: 'Invalid email or password'\n }\n });\n }\n\n const tokens = generateTokens(user);\n\n // Set refresh token as HTTP-only cookie\n res.cookie('refreshToken', tokens.refreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: 'strict',\n maxAge: 7 * 24 * 60 * 60 * 1000\n });\n\n res.json({\n accessToken: tokens.accessToken,\n expiresIn: 900, // 15 minutes in seconds\n tokenType: 'Bearer'\n });\n});\n\n// JWT verification middleware\nfunction authenticate(req, res, next) {\n const authHeader = req.headers.authorization;\n\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n return res.status(401).json({\n error: {\n code: 'auth_token_missing',\n message: 'Authorization header with Bearer token required'\n }\n });\n }\n\n const token = authHeader.slice(7);\n\n try {\n const payload = jwt.verify(token, JWT_SECRET, {\n issuer: 'api.example.com',\n audience: 'example.com'\n });\n\n req.user = payload;\n next();\n } catch (error) {\n if (error.name === 'TokenExpiredError') {\n return res.status(401).json({\n error: {\n code: 'auth_token_expired',\n message: 'Access token has expired. Please refresh.'\n }\n });\n }\n\n return res.status(401).json({\n error: {\n code: 'auth_token_invalid',\n message: 'Invalid access token'\n }\n });\n }\n}\n\n// Token refresh endpoint\napp.post('/auth/refresh', async (req, res) => {\n const refreshToken = req.cookies.refreshToken;\n\n if (!refreshToken) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_missing',\n message: 'Refresh token required'\n }\n });\n }\n\n try {\n const payload = jwt.verify(refreshToken, JWT_SECRET);\n\n // Check if token is revoked\n const isRevoked = await db.isTokenRevoked(refreshToken);\n if (isRevoked) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_revoked',\n message: 'Refresh token has been revoked'\n }\n });\n }\n\n const user = await db.findUser(payload.sub);\n const tokens = generateTokens(user);\n\n // Rotate refresh token\n await db.revokeToken(refreshToken);\n\n res.cookie('refreshToken', tokens.refreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: 'strict',\n maxAge: 7 * 24 * 60 * 60 * 1000\n });\n\n res.json({\n accessToken: tokens.accessToken,\n expiresIn: 900\n });\n } catch (error) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_invalid',\n message: 'Invalid refresh token'\n }\n });\n }\n});\n\n// Protected route\napp.get('/users/me', authenticate, (req, res) => {\n res.json({ userId: req.user.sub });\n});\n```\n\n```python\n# ✅ FastAPI with OAuth2 and JWT\nfrom fastapi import FastAPI, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm\nfrom jose import JWTError, jwt\nfrom passlib.context import CryptContext\nfrom datetime import datetime, timedelta\nimport os\n\napp = FastAPI()\n\nSECRET_KEY = os.getenv(\"JWT_SECRET\")\nALGORITHM = \"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES = 15\n\npwd_context = CryptContext(schemes=[\"bcrypt\"], deprecated=\"auto\")\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"auth/token\")\n\ndef create_access_token(data: dict, expires_delta: timedelta = None):\n to_encode = data.copy()\n expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))\n to_encode.update({\"exp\": expire, \"iss\": \"api.example.com\"})\n return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n\nasync def get_current_user(token: str = Depends(oauth2_scheme)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail={\"code\": \"auth_token_invalid\", \"message\": \"Invalid token\"},\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n user_id: str = payload.get(\"sub\")\n if user_id is None:\n raise credentials_exception\n except JWTError:\n raise credentials_exception\n\n user = await db.get_user(user_id)\n if user is None:\n raise credentials_exception\n return user\n\[email protected](\"/auth/token\")\nasync def login(form_data: OAuth2PasswordRequestForm = Depends()):\n user = await authenticate_user(form_data.username, form_data.password)\n if not user:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail={\"code\": \"invalid_credentials\", \"message\": \"Invalid credentials\"}\n )\n\n access_token = create_access_token(\n data={\"sub\": str(user.id)},\n expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\[email protected](\"/users/me\")\nasync def read_users_me(current_user = Depends(get_current_user)):\n return current_user\n```\n\n**Benefits:**\n- Short-lived tokens and refresh rotation limit damage from token theft\n- Bcrypt hashing protects passwords even if the database is compromised\n- HTTP-only cookies prevent XSS attacks on refresh tokens\n- OAuth 2.0/JWT are well-understood, widely supported standards\n- Stateless JWT tokens work well in distributed systems\n- Token-based auth provides clear user identification for audit logging\n\nReference: [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)\n\n\n---\n\n## Implement Proper Authorization\n\n**Impact: CRITICAL (Enforces access control and resource permissions)**\n\nAuthorization verifies what authenticated users can do. Implement role-based (RBAC) or attribute-based (ABAC) access control consistently.\n\n## Incorrect\n\n```javascript\n// ❌ No authorization checks\napp.delete('/users/:id', authenticate, async (req, res) => {\n // Anyone authenticated can delete any user!\n await db.deleteUser(req.params.id);\n res.status(204).send();\n});\n\n// ❌ Client-side only authorization\napp.get('/admin/users', authenticate, async (req, res) => {\n // Relies on frontend hiding the button\n const users = await db.getAllUsers();\n res.json(users);\n});\n\n// ❌ Inconsistent checks\napp.get('/documents/:id', async (req, res) => {\n const doc = await db.findDocument(req.params.id);\n // Sometimes checks, sometimes doesn't\n if (doc.isPublic) {\n res.json(doc);\n }\n // Private docs accessible without check!\n res.json(doc);\n});\n```\n\n**Problems:**\n- Any authenticated user can perform any operation on any resource\n- Client-side authorization can be bypassed by calling the API directly\n- Inconsistent checks leave gaps attackers can exploit\n- No separation between authentication (who) and authorization (what)\n- No audit trail of permission-based access decisions\n\n## Correct\n\n```javascript\n// ✅ Role-Based Access Control (RBAC)\nconst ROLES = {\n ADMIN: 'admin',\n MANAGER: 'manager',\n USER: 'user'\n};\n\nconst PERMISSIONS = {\n // Resource: action -> roles that can perform it\n users: {\n read: [ROLES.ADMIN, ROLES.MANAGER, ROLES.USER],\n create: [ROLES.ADMIN, ROLES.MANAGER],\n update: [ROLES.ADMIN, ROLES.MANAGER],\n delete: [ROLES.ADMIN]\n },\n reports: {\n read: [ROLES.ADMIN, ROLES.MANAGER],\n create: [ROLES.ADMIN, ROLES.MANAGER],\n delete: [ROLES.ADMIN]\n },\n settings: {\n read: [ROLES.ADMIN],\n update: [ROLES.ADMIN]\n }\n};\n\n// Authorization middleware\nfunction authorize(resource, action) {\n return (req, res, next) => {\n const userRoles = req.user.roles || [];\n const allowedRoles = PERMISSIONS[resource]?.[action] || [];\n\n const hasPermission = userRoles.some(role =>\n allowedRoles.includes(role)\n );\n\n if (!hasPermission) {\n return res.status(403).json({\n error: {\n code: 'forbidden',\n message: `You don't have permission to ${action} ${resource}`,\n requiredRoles: allowedRoles,\n yourRoles: userRoles\n }\n });\n }\n\n next();\n };\n}\n\n// Resource ownership check\nasync function authorizeOwnership(req, res, next) {\n const resourceId = req.params.id;\n const userId = req.user.sub;\n\n const resource = await db.findResource(resourceId);\n\n if (!resource) {\n return res.status(404).json({\n error: { code: 'not_found', message: 'Resource not found' }\n });\n }\n\n // Admin can access anything\n if (req.user.roles.includes(ROLES.ADMIN)) {\n req.resource = resource;\n return next();\n }\n\n // Owner can access their own resources\n if (resource.ownerId !== userId) {\n return res.status(403).json({\n error: {\n code: 'forbidden',\n message: 'You can only access your own resources'\n }\n });\n }\n\n req.resource = resource;\n next();\n}\n\n// Usage\napp.get('/users',\n authenticate,\n authorize('users', 'read'),\n async (req, res) => {\n const users = await db.getUsers();\n res.json(users);\n }\n);\n\napp.delete('/users/:id',\n authenticate,\n authorize('users', 'delete'),\n async (req, res) => {\n await db.deleteUser(req.params.id);\n res.status(204).send();\n }\n);\n\napp.get('/documents/:id',\n authenticate,\n authorizeOwnership,\n async (req, res) => {\n res.json(req.resource);\n }\n);\n\napp.put('/documents/:id',\n authenticate,\n authorizeOwnership,\n async (req, res) => {\n const updated = await db.updateDocument(req.params.id, req.body);\n res.json(updated);\n }\n);\n```\n\n```python\n# ✅ FastAPI with RBAC\nfrom fastapi import FastAPI, Depends, HTTPException, status\nfrom enum import Enum\nfrom typing import List\nfrom functools import wraps\n\napp = FastAPI()\n\nclass Role(str, Enum):\n ADMIN = \"admin\"\n MANAGER = \"manager\"\n USER = \"user\"\n\nclass Permission(str, Enum):\n READ_USERS = \"read:users\"\n WRITE_USERS = \"write:users\"\n DELETE_USERS = \"delete:users\"\n READ_REPORTS = \"read:reports\"\n ADMIN_SETTINGS = \"admin:settings\"\n\nROLE_PERMISSIONS = {\n Role.ADMIN: [\n Permission.READ_USERS,\n Permission.WRITE_USERS,\n Permission.DELETE_USERS,\n Permission.READ_REPORTS,\n Permission.ADMIN_SETTINGS\n ],\n Role.MANAGER: [\n Permission.READ_USERS,\n Permission.WRITE_USERS,\n Permission.READ_REPORTS\n ],\n Role.USER: [\n Permission.READ_USERS\n ]\n}\n\ndef get_user_permissions(user) -> List[Permission]:\n permissions = set()\n for role in user.roles:\n permissions.update(ROLE_PERMISSIONS.get(role, []))\n return list(permissions)\n\ndef require_permission(permission: Permission):\n def decorator(func):\n @wraps(func)\n async def wrapper(*args, current_user = Depends(get_current_user), **kwargs):\n user_permissions = get_user_permissions(current_user)\n\n if permission not in user_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail={\n \"code\": \"forbidden\",\n \"message\": f\"Permission '{permission.value}' required\",\n \"your_permissions\": [p.value for p in user_permissions]\n }\n )\n return await func(*args, current_user=current_user, **kwargs)\n return wrapper\n return decorator\n\ndef require_ownership_or_admin(resource_type: str):\n async def check_ownership(\n resource_id: int,\n current_user = Depends(get_current_user)\n ):\n resource = await db.get_resource(resource_type, resource_id)\n\n if not resource:\n raise HTTPException(status_code=404, detail=\"Not found\")\n\n if Role.ADMIN in current_user.roles:\n return resource\n\n if resource.owner_id != current_user.id:\n raise HTTPException(\n status_code=403,\n detail={\n \"code\": \"forbidden\",\n \"message\": \"You can only access your own resources\"\n }\n )\n return resource\n return check_ownership\n\[email protected](\"/users\")\n@require_permission(Permission.READ_USERS)\nasync def list_users(current_user = Depends(get_current_user)):\n return await db.get_users()\n\[email protected](\"/users/{user_id}\")\n@require_permission(Permission.DELETE_USERS)\nasync def delete_user(user_id: int, current_user = Depends(get_current_user)):\n await db.delete_user(user_id)\n return {\"deleted\": True}\n\[email protected](\"/documents/{document_id}\")\nasync def get_document(\n document = Depends(require_ownership_or_admin(\"documents\"))\n):\n return document\n```\n\n```json\n// ✅ Authorization error response\n{\n \"error\": {\n \"code\": \"forbidden\",\n \"message\": \"You don't have permission to delete users\",\n \"details\": {\n \"requiredPermission\": \"delete:users\",\n \"yourRoles\": [\"user\", \"manager\"],\n \"requiredRoles\": [\"admin\"]\n }\n }\n}\n```\n\n## Authorization Patterns\n\n| Pattern | Use Case | Example |\n|---------|----------|---------|\n| RBAC | Role-based access | Admin, Manager, User roles |\n| ABAC | Attribute-based | Department, location, time-based |\n| Ownership | Resource owners | User owns their documents |\n| Hierarchical | Org structure | Managers see team's data |\n| Feature flags | Feature access | Premium features |\n\n**Benefits:**\n- Prevents unauthorized access to sensitive data and operations\n- Principle of least privilege: users only get access they need\n- Centralized permission definitions are easy to audit and update\n- Server-side checks cannot be bypassed like client-side authorization\n- RBAC/ABAC scales better than per-user permissions\n- Clear authorization rules support compliance requirements\n\nReference: [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)\n\n\n---\n\n## Implement Rate Limiting\n\n**Impact: CRITICAL (Prevents abuse and ensures service availability)**\n\nProtect your API from abuse by limiting the number of requests clients can make within a time window.\n\n## Incorrect\n\n```javascript\n// ❌ No rate limiting\napp.post('/login', async (req, res) => {\n // Vulnerable to brute force attacks\n const user = await authenticate(req.body);\n res.json(user);\n});\n\n// ❌ Rate limit only on response\napp.get('/api/data', async (req, res) => {\n const data = await expensiveQuery(); // Query runs every time!\n if (requestCount > 100) {\n res.status(429).json({ error: 'Too many requests' });\n }\n res.json(data);\n});\n\n// ❌ No rate limit headers\napp.use((req, res, next) => {\n if (isRateLimited(req)) {\n res.status(429).send('Too many requests');\n // Client doesn't know when to retry\n }\n next();\n});\n```\n\n**Problems:**\n- Login endpoints without rate limits are vulnerable to brute force attacks\n- Checking limits after processing wastes server resources\n- Missing retry headers leave clients guessing when they can retry\n- No per-user or per-endpoint differentiation allows abuse\n- Expensive operations run before limits are checked\n\n## Correct\n\n```javascript\n// ✅ Comprehensive rate limiting\nconst rateLimit = require('express-rate-limit');\nconst RedisStore = require('rate-limit-redis');\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\n\n// General API rate limiter\nconst apiLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:api:'\n }),\n windowMs: 60 * 1000, // 1 minute\n max: 100, // 100 requests per minute\n standardHeaders: true, // Return rate limit info in headers\n legacyHeaders: false,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Too many requests. Please slow down.',\n retryAfter: 60\n }\n },\n keyGenerator: (req) => {\n // Rate limit by user ID if authenticated, otherwise by IP\n return req.user?.id || req.ip;\n }\n});\n\n// Stricter limiter for auth endpoints\nconst authLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:auth:'\n }),\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: 5, // 5 attempts per 15 minutes\n standardHeaders: true,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Too many login attempts. Please try again later.',\n retryAfter: 900\n }\n },\n keyGenerator: (req) => req.body.email || req.ip // Per email address\n});\n\n// Expensive endpoint limiter\nconst expensiveLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:expensive:'\n }),\n windowMs: 60 * 60 * 1000, // 1 hour\n max: 10, // 10 requests per hour\n standardHeaders: true,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Export limit reached. Try again in an hour.',\n retryAfter: 3600\n }\n }\n});\n\n// Apply rate limiters\napp.use('/api/', apiLimiter);\napp.use('/auth/login', authLimiter);\napp.use('/auth/password-reset', authLimiter);\napp.post('/api/export', expensiveLimiter, exportHandler);\n\n// Custom sliding window implementation\nclass SlidingWindowRateLimiter {\n constructor(redis, options) {\n this.redis = redis;\n this.windowMs = options.windowMs;\n this.maxRequests = options.max;\n }\n\n async isAllowed(key) {\n const now = Date.now();\n const windowStart = now - this.windowMs;\n\n const pipeline = this.redis.pipeline();\n\n // Remove old entries\n pipeline.zremrangebyscore(key, 0, windowStart);\n\n // Count requests in window\n pipeline.zcard(key);\n\n // Add current request\n pipeline.zadd(key, now, `${now}-${Math.random()}`);\n\n // Set expiry\n pipeline.expire(key, Math.ceil(this.windowMs / 1000));\n\n const results = await pipeline.exec();\n const count = results[1][1];\n\n return {\n allowed: count \u003c this.maxRequests,\n remaining: Math.max(0, this.maxRequests - count - 1),\n resetAt: new Date(now + this.windowMs)\n };\n }\n}\n\n// Tiered rate limiting based on plan\nconst tierLimits = {\n free: { requestsPerMinute: 60, requestsPerDay: 1000 },\n basic: { requestsPerMinute: 300, requestsPerDay: 10000 },\n pro: { requestsPerMinute: 1000, requestsPerDay: 100000 },\n enterprise: { requestsPerMinute: 5000, requestsPerDay: 1000000 }\n};\n\nasync function tieredRateLimiter(req, res, next) {\n const user = req.user;\n const tier = user?.plan || 'free';\n const limits = tierLimits[tier];\n\n const minuteKey = `rl:${user?.id || req.ip}:minute`;\n const dayKey = `rl:${user?.id || req.ip}:day`;\n\n // Check minute limit\n const minuteCount = await redis.incr(minuteKey);\n if (minuteCount === 1) {\n await redis.expire(minuteKey, 60);\n }\n\n // Check daily limit\n const dayCount = await redis.incr(dayKey);\n if (dayCount === 1) {\n await redis.expire(dayKey, 86400);\n }\n\n // Set rate limit headers\n res.set('X-RateLimit-Limit', limits.requestsPerMinute);\n res.set('X-RateLimit-Remaining', Math.max(0, limits.requestsPerMinute - minuteCount));\n res.set('X-RateLimit-Reset', Math.ceil(Date.now() / 1000) + 60);\n\n if (minuteCount > limits.requestsPerMinute) {\n return res.status(429).json({\n error: {\n code: 'rate_limit_exceeded',\n message: 'Minute rate limit exceeded',\n limit: limits.requestsPerMinute,\n window: '1 minute',\n retryAfter: await redis.ttl(minuteKey)\n }\n });\n }\n\n if (dayCount > limits.requestsPerDay) {\n return res.status(429).json({\n error: {\n code: 'rate_limit_exceeded',\n message: 'Daily rate limit exceeded',\n limit: limits.requestsPerDay,\n window: '24 hours',\n upgradeUrl: '/pricing'\n }\n });\n }\n\n next();\n}\n```\n\n```python\n# ✅ FastAPI with rate limiting\nfrom fastapi import FastAPI, Request, HTTPException\nfrom slowapi import Limiter, _rate_limit_exceeded_handler\nfrom slowapi.util import get_remote_address\nfrom slowapi.errors import RateLimitExceeded\n\nlimiter = Limiter(key_func=get_remote_address)\napp = FastAPI()\napp.state.limiter = limiter\napp.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)\n\[email protected](\"/api/data\")\[email protected](\"100/minute\")\nasync def get_data(request: Request):\n return {\"data\": \"value\"}\n\[email protected](\"/auth/login\")\[email protected](\"5/15minutes\")\nasync def login(request: Request):\n return {\"token\": \"...\"}\n\n# Custom user-based rate limiting\nasync def get_rate_limit_key(request: Request):\n if request.user:\n return f\"user:{request.user.id}\"\n return f\"ip:{request.client.host}\"\n\[email protected](\"/api/premium\")\[email protected](\"1000/minute\", key_func=get_rate_limit_key)\nasync def premium_endpoint(request: Request):\n return {\"data\": \"premium\"}\n```\n\n## Rate Limit Headers\n\n```\nHTTP/1.1 200 OK\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 95\nX-RateLimit-Reset: 1705312800\nRetry-After: 60\n```\n\n**Benefits:**\n- Prevents denial-of-service attacks from overwhelming servers\n- Limits brute force password guessing and credential stuffing attacks\n- Ensures fair API access for all users\n- Prevents runaway usage that increases infrastructure costs\n- Enables tiered pricing based on usage limits\n- Rate limit headers help clients implement proper backoff strategies\n\nReference: [IETF Rate Limiting Headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)\n\n\n---\n\n## Validate All Input Data\n\n**Impact: CRITICAL (Prevents injection attacks and malformed data)**\n\nNever trust client input. Validate, sanitize, and constrain all incoming data to prevent security vulnerabilities.\n\n## Incorrect\n\n```javascript\n// ❌ No validation\napp.post('/users', async (req, res) => {\n // Directly using user input!\n const user = await db.createUser(req.body);\n res.json(user);\n});\n\n// ❌ SQL injection vulnerability\napp.get('/users', async (req, res) => {\n const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;\n const users = await db.raw(query);\n res.json(users);\n});\n\n// ❌ NoSQL injection\napp.post('/login', async (req, res) => {\n const user = await db.users.findOne({\n email: req.body.email,\n password: req.body.password // Could be { $gt: '' }\n });\n});\n\n// ❌ Path traversal\napp.get('/files/:filename', (req, res) => {\n const path = `./uploads/${req.params.filename}`;\n res.sendFile(path); // Could access ../../../etc/passwd\n});\n```\n\n**Problems:**\n- SQL injection allows attackers to read, modify, or delete database data\n- NoSQL injection bypasses authentication with operator-based payloads\n- Path traversal exposes sensitive server files\n- Unbounded input sizes enable memory exhaustion and DoS attacks\n- Unsanitized HTML/script input enables XSS attacks\n- Type confusion attacks exploit loosely typed inputs\n\n## Correct\n\n```javascript\n// ✅ Comprehensive input validation\nconst { body, param, query, validationResult } = require('express-validator');\nconst sanitizeHtml = require('sanitize-html');\nconst path = require('path');\n\n// Validation middleware\nconst validateUser = [\n body('email')\n .trim()\n .notEmpty().withMessage('Email is required')\n .isEmail().withMessage('Must be a valid email')\n .normalizeEmail()\n .isLength({ max: 255 }).withMessage('Email too long'),\n\n body('password')\n .notEmpty().withMessage('Password is required')\n .isLength({ min: 8, max: 100 }).withMessage('Password must be 8-100 characters')\n .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/).withMessage('Password too weak'),\n\n body('name')\n .trim()\n .notEmpty().withMessage('Name is required')\n .isLength({ min: 1, max: 100 }).withMessage('Name must be 1-100 characters')\n .matches(/^[a-zA-Z\\s'-]+$/).withMessage('Name contains invalid characters')\n .customSanitizer(value => sanitizeHtml(value, { allowedTags: [] })),\n\n body('age')\n .optional()\n .isInt({ min: 0, max: 150 }).withMessage('Age must be 0-150')\n .toInt(),\n\n body('website')\n .optional()\n .trim()\n .isURL({ protocols: ['http', 'https'] }).withMessage('Invalid URL')\n];\n\n// Validation error handler\nconst handleValidation = (req, res, next) => {\n const errors = validationResult(req);\n if (!errors.isEmpty()) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Invalid input data',\n details: errors.array().map(e => ({\n field: e.path,\n message: e.msg,\n value: e.value\n }))\n }\n });\n }\n next();\n};\n\napp.post('/users', validateUser, handleValidation, async (req, res) => {\n const user = await db.createUser(req.body);\n res.status(201).json(user);\n});\n\n// ✅ Parameterized queries (prevent SQL injection)\napp.get('/users', async (req, res) => {\n const users = await db.query(\n 'SELECT id, name, email FROM users WHERE name = ?',\n [req.query.name]\n );\n res.json(users);\n});\n\n// ✅ Safe MongoDB queries\napp.post('/login', async (req, res) => {\n // Ensure email and password are strings\n const email = String(req.body.email || '');\n const password = String(req.body.password || '');\n\n const user = await db.users.findOne({ email });\n if (!user || !await bcrypt.compare(password, user.passwordHash)) {\n return res.status(401).json({ error: 'Invalid credentials' });\n }\n // ...\n});\n\n// ✅ Path traversal prevention\napp.get('/files/:filename',\n param('filename')\n .matches(/^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9]+$/)\n .withMessage('Invalid filename'),\n handleValidation,\n (req, res) => {\n const uploadsDir = path.resolve('./uploads');\n const filePath = path.join(uploadsDir, req.params.filename);\n\n // Ensure path is within uploads directory\n if (!filePath.startsWith(uploadsDir)) {\n return res.status(400).json({ error: 'Invalid path' });\n }\n\n res.sendFile(filePath);\n }\n);\n\n// ✅ Array size limits\napp.post('/batch',\n body('items')\n .isArray({ min: 1, max: 100 })\n .withMessage('Items must be array of 1-100 elements'),\n body('items.*.id')\n .isUUID()\n .withMessage('Each item must have valid UUID'),\n handleValidation,\n async (req, res) => {\n const results = await processBatch(req.body.items);\n res.json(results);\n }\n);\n\n// ✅ JSON depth/size limits\nconst jsonParser = express.json({\n limit: '100kb', // Max request body size\n strict: true // Only accept arrays and objects\n});\n\napp.use('/api', jsonParser);\n```\n\n```python\n# ✅ FastAPI with Pydantic validation\nfrom fastapi import FastAPI, Query, Path, Body, HTTPException\nfrom pydantic import BaseModel, EmailStr, Field, validator, HttpUrl\nfrom typing import Optional, List\nimport re\nimport bleach\n\napp = FastAPI()\n\nclass UserCreate(BaseModel):\n email: EmailStr\n password: str = Field(..., min_length=8, max_length=100)\n name: str = Field(..., min_length=1, max_length=100)\n age: Optional[int] = Field(None, ge=0, le=150)\n website: Optional[HttpUrl] = None\n\n @validator('password')\n def password_strength(cls, v):\n if not re.search(r'[A-Z]', v):\n raise ValueError('Must contain uppercase letter')\n if not re.search(r'[a-z]', v):\n raise ValueError('Must contain lowercase letter')\n if not re.search(r'\\d', v):\n raise ValueError('Must contain digit')\n return v\n\n @validator('name')\n def sanitize_name(cls, v):\n # Only allow letters, spaces, hyphens, apostrophes\n if not re.match(r\"^[a-zA-Z\\s'-]+$\", v):\n raise ValueError('Name contains invalid characters')\n # Sanitize HTML\n return bleach.clean(v, tags=[], strip=True)\n\nclass BatchRequest(BaseModel):\n items: List[str] = Field(..., min_items=1, max_items=100)\n\n @validator('items', each_item=True)\n def validate_uuid(cls, v):\n if not re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}

API Design Patterns RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation. Metadata - Version: 2.0.0 - Rule Count: 38 rules across 7 categories - License: MIT When to Apply Reference these guidelines when: - Designing new API endpoints - Reviewing existing API structure - Implementing error handling and validation - Setting up pagination, filtering, and sorting - Planning API versioning strategy - Configuring API securit…

, v, re.I):\n raise ValueError('Invalid UUID format')\n return v\n\[email protected](\"/users\")\nasync def create_user(user: UserCreate):\n return await db.create_user(user.dict())\n\[email protected](\"/users/{user_id}\")\nasync def get_user(\n user_id: int = Path(..., ge=1, le=2147483647, description=\"User ID\")\n):\n return await db.get_user(user_id)\n\[email protected](\"/search\")\nasync def search(\n q: str = Query(..., min_length=1, max_length=100),\n page: int = Query(1, ge=1, le=1000),\n limit: int = Query(20, ge=1, le=100)\n):\n # q is automatically validated and constrained\n return await db.search(q, page, limit)\n```\n\n## Input Validation Checklist\n\n| Check | Why |\n|-------|-----|\n| Type validation | Prevent type confusion attacks |\n| Length limits | Prevent buffer overflows, DoS |\n| Character whitelist | Prevent injection attacks |\n| Range validation | Ensure business logic integrity |\n| Format validation | Email, URL, UUID patterns |\n| Sanitization | Remove/escape dangerous content |\n| Array size limits | Prevent memory exhaustion |\n| Nested depth limits | Prevent stack overflow |\n\n**Benefits:**\n- Prevents SQL/NoSQL injection attacks from manipulating queries\n- Sanitizing input stops cross-site scripting (XSS) attacks\n- Filename validation prevents unauthorized file access via path traversal\n- Size limits prevent memory exhaustion and denial-of-service attacks\n- Ensures only valid data enters your system, maintaining data integrity\n- Catches bad data at the API boundary before it causes downstream errors\n\nReference: [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)\n\n\n---\n\n## Configure CORS Properly\n\n**Impact: CRITICAL (Prevents unauthorized cross-origin access)**\n\nCross-Origin Resource Sharing (CORS) must be configured correctly to allow legitimate cross-origin requests while preventing unauthorized access.\n\n## Incorrect\n\n```javascript\n// ❌ Allow all origins\napp.use(cors({\n origin: '*',\n credentials: true // DANGEROUS: Can't use * with credentials!\n}));\n\n// ❌ Reflecting origin without validation\napp.use((req, res, next) => {\n // Allows ANY origin - security vulnerability\n res.header('Access-Control-Allow-Origin', req.headers.origin);\n res.header('Access-Control-Allow-Credentials', 'true');\n next();\n});\n\n// ❌ Overly permissive headers\napp.use(cors({\n origin: '*',\n methods: '*',\n allowedHeaders: '*',\n exposedHeaders: '*'\n}));\n\n// ❌ Missing CORS entirely for API\napp.get('/api/data', (req, res) => {\n // Browser will block cross-origin requests\n res.json({ data: 'value' });\n});\n```\n\n**Problems:**\n- Wildcard origin with credentials allows any website to make authenticated requests\n- Reflecting the Origin header blindly defeats the purpose of CORS\n- Overly permissive headers expose the API to cross-origin attacks\n- Missing CORS configuration blocks legitimate frontend applications\n- Enables CSRF attacks that rely on cross-origin requests\n\n## Correct\n\n```javascript\n// ✅ Properly configured CORS\nconst cors = require('cors');\n\n// Allowed origins whitelist\nconst allowedOrigins = [\n 'https://myapp.com',\n 'https://www.myapp.com',\n 'https://admin.myapp.com'\n];\n\n// Development origins (only in non-production)\nif (process.env.NODE_ENV !== 'production') {\n allowedOrigins.push(\n 'http://localhost:3000',\n 'http://localhost:8080'\n );\n}\n\n// CORS configuration\nconst corsOptions = {\n origin: (origin, callback) => {\n // Allow requests with no origin (mobile apps, curl, etc.)\n if (!origin) {\n return callback(null, true);\n }\n\n if (allowedOrigins.includes(origin)) {\n callback(null, true);\n } else {\n callback(new Error('Not allowed by CORS'));\n }\n },\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n allowedHeaders: [\n 'Content-Type',\n 'Authorization',\n 'X-Request-ID',\n 'X-Requested-With'\n ],\n exposedHeaders: [\n 'X-Request-ID',\n 'X-RateLimit-Limit',\n 'X-RateLimit-Remaining',\n 'X-RateLimit-Reset'\n ],\n credentials: true,\n maxAge: 86400, // 24 hours - cache preflight requests\n optionsSuccessStatus: 204\n};\n\napp.use(cors(corsOptions));\n\n// Handle CORS errors\napp.use((err, req, res, next) => {\n if (err.message === 'Not allowed by CORS') {\n return res.status(403).json({\n error: {\n code: 'cors_error',\n message: 'Cross-origin request blocked',\n origin: req.headers.origin\n }\n });\n }\n next(err);\n});\n\n// Route-specific CORS (for public endpoints)\nconst publicCors = cors({\n origin: '*',\n methods: ['GET'],\n maxAge: 86400\n});\n\napp.get('/api/public/health', publicCors, (req, res) => {\n res.json({ status: 'ok' });\n});\n\n// Strict CORS for sensitive endpoints\nconst strictCors = cors({\n origin: 'https://admin.myapp.com',\n credentials: true,\n methods: ['GET', 'POST', 'DELETE']\n});\n\napp.use('/api/admin', strictCors, adminRouter);\n```\n\n```python\n# ✅ FastAPI CORS configuration\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nimport os\n\napp = FastAPI()\n\n# Define allowed origins\nallowed_origins = [\n \"https://myapp.com\",\n \"https://www.myapp.com\",\n \"https://admin.myapp.com\"\n]\n\n# Add development origins\nif os.getenv(\"ENVIRONMENT\") != \"production\":\n allowed_origins.extend([\n \"http://localhost:3000\",\n \"http://localhost:8080\"\n ])\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=allowed_origins,\n allow_credentials=True,\n allow_methods=[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n allow_headers=[\n \"Content-Type\",\n \"Authorization\",\n \"X-Request-ID\",\n \"X-Requested-With\"\n ],\n expose_headers=[\n \"X-Request-ID\",\n \"X-RateLimit-Limit\",\n \"X-RateLimit-Remaining\"\n ],\n max_age=86400\n)\n\n# For public endpoints, create a sub-application\npublic_app = FastAPI()\npublic_app.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Public API\n allow_methods=[\"GET\"],\n max_age=86400\n)\n\napp.mount(\"/public\", public_app)\n```\n\n```typescript\n// ✅ Dynamic CORS based on subdomain pattern\nconst corsOptions = {\n origin: (origin: string | undefined, callback: Function) => {\n if (!origin) {\n return callback(null, true);\n }\n\n // Allow all subdomains of myapp.com\n const allowedPattern = /^https:\\/\\/([a-z0-9-]+\\.)?myapp\\.com$/;\n\n if (allowedPattern.test(origin)) {\n callback(null, true);\n } else {\n callback(new Error('CORS not allowed'));\n }\n },\n credentials: true\n};\n```\n\n## CORS Headers Reference\n\n| Header | Purpose | Example |\n|--------|---------|---------|\n| Access-Control-Allow-Origin | Allowed origins | `https://myapp.com` |\n| Access-Control-Allow-Methods | Allowed HTTP methods | `GET, POST, PUT` |\n| Access-Control-Allow-Headers | Allowed request headers | `Content-Type, Authorization` |\n| Access-Control-Expose-Headers | Headers client can read | `X-Request-ID` |\n| Access-Control-Allow-Credentials | Allow cookies/auth | `true` |\n| Access-Control-Max-Age | Preflight cache time | `86400` |\n\n## Common Patterns\n\n```javascript\n// ✅ Pattern 1: Subdomain allowlist\nconst isAllowedOrigin = (origin) => {\n if (!origin) return true;\n const url = new URL(origin);\n return url.hostname.endsWith('.myapp.com');\n};\n\n// ✅ Pattern 2: Environment-based\nconst origins = {\n production: ['https://myapp.com'],\n staging: ['https://staging.myapp.com'],\n development: ['http://localhost:3000']\n};\nconst allowedOrigins = origins[process.env.NODE_ENV];\n\n// ✅ Pattern 3: Database-driven (for multi-tenant)\nconst corsOptions = {\n origin: async (origin, callback) => {\n const tenant = await db.findTenantByDomain(origin);\n callback(null, tenant?.corsEnabled ?? false);\n }\n};\n```\n\n**Benefits:**\n- Prevents malicious websites from making unauthorized API calls\n- Proper configuration prevents credential leakage to untrusted origins\n- Blocks CSRF attacks that rely on cross-origin requests\n- Explicitly controls which domains can access your API\n- Preflight caching reduces OPTIONS request overhead\n- Route-specific CORS allows different policies for different endpoints\n\nReference: [MDN CORS Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\n\n\n---\n\n## Enforce HTTPS Only\n\n**Impact: CRITICAL (Protects data in transit from interception)**\n\nAll API traffic must use HTTPS to encrypt data in transit. Never allow unencrypted HTTP connections for APIs.\n\n## Incorrect\n\n```javascript\n// ❌ HTTP server without TLS\nconst http = require('http');\nconst app = require('./app');\n\nhttp.createServer(app).listen(80, () => {\n console.log('API running on http://localhost:80');\n // Credentials transmitted in plain text!\n});\n\n// ❌ Optional HTTPS\nif (process.env.USE_HTTPS === 'true') {\n // HTTPS is optional, not enforced\n}\n\n// ❌ No redirect from HTTP to HTTPS\napp.get('/', (req, res) => {\n // Allows HTTP access\n res.json({ message: 'Welcome' });\n});\n\n// ❌ Insecure cookie settings\nres.cookie('session', token, {\n secure: false, // Sent over HTTP!\n httpOnly: true\n});\n```\n\n```json\n// ❌ HTTP URLs in responses\n{\n \"user\": {\n \"id\": 123,\n \"avatar\": \"http://cdn.example.com/avatar.jpg\",\n \"profile\": \"http://api.example.com/users/123\"\n }\n}\n```\n\n**Problems:**\n- Credentials and tokens transmitted in plain text can be intercepted\n- Man-in-the-middle attacks can modify data in transit\n- Cookies without the secure flag are sent over unencrypted connections\n- HTTP URLs in responses encourage insecure client behavior\n- Violates compliance requirements (PCI-DSS, HIPAA, GDPR)\n\n## Correct\n\n```javascript\n// ✅ HTTPS with security headers\nconst https = require('https');\nconst fs = require('fs');\nconst express = require('express');\nconst helmet = require('helmet');\n\nconst app = express();\n\n// Security headers including HSTS\napp.use(helmet({\n hsts: {\n maxAge: 31536000, // 1 year\n includeSubDomains: true,\n preload: true\n }\n}));\n\n// Force HTTPS redirect (for direct access)\napp.use((req, res, next) => {\n if (!req.secure && req.get('x-forwarded-proto') !== 'https') {\n return res.redirect(301, `https://${req.get('host')}${req.url}`);\n }\n next();\n});\n\n// Secure cookie settings\napp.use((req, res, next) => {\n res.cookie = function(name, value, options = {}) {\n options.secure = true; // HTTPS only\n options.httpOnly = true; // No JavaScript access\n options.sameSite = 'strict'; // CSRF protection\n return res.cookie.call(this, name, value, options);\n };\n next();\n});\n\n// HTTPS server\nconst options = {\n key: fs.readFileSync('/path/to/private.key'),\n cert: fs.readFileSync('/path/to/certificate.crt'),\n ca: fs.readFileSync('/path/to/ca-bundle.crt'),\n minVersion: 'TLSv1.2', // Minimum TLS version\n ciphers: [\n 'ECDHE-ECDSA-AES128-GCM-SHA256',\n 'ECDHE-RSA-AES128-GCM-SHA256',\n 'ECDHE-ECDSA-AES256-GCM-SHA384',\n 'ECDHE-RSA-AES256-GCM-SHA384'\n ].join(':')\n};\n\nhttps.createServer(options, app).listen(443, () => {\n console.log('Secure API running on https://localhost:443');\n});\n\n// Also listen on HTTP just to redirect\nconst http = require('http');\nhttp.createServer((req, res) => {\n res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });\n res.end();\n}).listen(80);\n```\n\n```python\n# ✅ FastAPI with HTTPS enforcement\nfrom fastapi import FastAPI, Request\nfrom fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware\nfrom fastapi.middleware.trustedhost import TrustedHostMiddleware\nfrom starlette.middleware.base import BaseHTTPMiddleware\n\napp = FastAPI()\n\n# Redirect HTTP to HTTPS\napp.add_middleware(HTTPSRedirectMiddleware)\n\n# Only allow specific hosts\napp.add_middleware(\n TrustedHostMiddleware,\n allowed_hosts=[\"api.example.com\", \"*.example.com\"]\n)\n\n# Add HSTS header\nclass HSTSMiddleware(BaseHTTPMiddleware):\n async def dispatch(self, request: Request, call_next):\n response = await call_next(request)\n response.headers[\"Strict-Transport-Security\"] = (\n \"max-age=31536000; includeSubDomains; preload\"\n )\n return response\n\napp.add_middleware(HSTSMiddleware)\n\n# Secure cookie response\nfrom fastapi.responses import JSONResponse\n\[email protected](\"/auth/login\")\nasync def login(credentials: Credentials):\n token = create_token(credentials)\n response = JSONResponse({\"status\": \"logged_in\"})\n response.set_cookie(\n key=\"session\",\n value=token,\n httponly=True,\n secure=True,\n samesite=\"strict\",\n max_age=86400\n )\n return response\n```\n\n```nginx\n# ✅ Nginx HTTPS configuration\nserver {\n listen 80;\n server_name api.example.com;\n return 301 https://$server_name$request_uri;\n}\n\nserver {\n listen 443 ssl http2;\n server_name api.example.com;\n\n ssl_certificate /etc/ssl/certs/certificate.crt;\n ssl_certificate_key /etc/ssl/private/private.key;\n\n # Modern TLS configuration\n ssl_protocols TLSv1.2 TLSv1.3;\n ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;\n ssl_prefer_server_ciphers on;\n\n # HSTS\n add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n\n # Security headers\n add_header X-Content-Type-Options \"nosniff\" always;\n add_header X-Frame-Options \"DENY\" always;\n add_header X-XSS-Protection \"1; mode=block\" always;\n\n location / {\n proxy_pass http://localhost:3000;\n proxy_set_header X-Forwarded-Proto https;\n }\n}\n```\n\n```yaml\n# ✅ Docker Compose with Traefik for automatic HTTPS\nversion: '3.8'\nservices:\n traefik:\n image: traefik:v2.10\n command:\n - \"--providers.docker=true\"\n - \"--entrypoints.web.address=:80\"\n - \"--entrypoints.websecure.address=:443\"\n - \"--entrypoints.web.http.redirections.entryPoint.to=websecure\"\n - \"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web\"\n - \"[email protected]\"\n - \"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json\"\n ports:\n - \"80:80\"\n - \"443:443\"\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - letsencrypt:/letsencrypt\n\n api:\n image: my-api\n labels:\n - \"traefik.http.routers.api.rule=Host(`api.example.com`)\"\n - \"traefik.http.routers.api.tls.certresolver=letsencrypt\"\n - \"traefik.http.middlewares.hsts.headers.stsSeconds=31536000\"\n - \"traefik.http.routers.api.middlewares=hsts\"\n```\n\n## TLS Configuration Checklist\n\n| Setting | Recommendation |\n|---------|----------------|\n| Minimum TLS | TLSv1.2 |\n| Preferred TLS | TLSv1.3 |\n| HSTS max-age | 31536000 (1 year) |\n| includeSubDomains | Yes |\n| HSTS preload | Yes (after testing) |\n| Certificate | Valid, not self-signed |\n| Certificate chain | Complete |\n\n**Benefits:**\n- HTTPS encrypts all data in transit, protecting credentials and sensitive data\n- TLS certificates verify server identity, preventing MITM attacks\n- Data integrity ensures content is not modified in transit\n- Meets compliance requirements for PCI-DSS, HIPAA, and GDPR\n- HTTP/2 and many modern web APIs require HTTPS\n- Secure cookies only work over HTTPS connections\n\nReference: [OWASP Transport Layer Protection](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html)\n\n\n---\n\n## Protect Sensitive Data in Responses\n\n**Impact: CRITICAL (Prevents data leaks and privacy violations)**\n\nNever expose sensitive information like passwords, tokens, internal IDs, or PII in API responses.\n\n## Incorrect\n\n```json\n// ❌ Exposing password hash\n{\n \"id\": 123,\n \"email\": \"[email protected]\",\n \"passwordHash\": \"$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.n\",\n \"name\": \"John Doe\"\n}\n\n// ❌ Exposing API keys\n{\n \"user\": {\n \"id\": 123,\n \"apiKey\": \"sk_live_abc123xyz789\",\n \"secretKey\": \"whsec_secret_key_here\"\n }\n}\n\n// ❌ Exposing internal IDs\n{\n \"user\": {\n \"id\": 123,\n \"internalDatabaseId\": \"mongo_507f1f77bcf86cd799439011\",\n \"stripeCustomerId\": \"cus_abc123\"\n }\n}\n\n// ❌ Exposing other users' data\n{\n \"order\": {\n \"id\": 456,\n \"customer\": {\n \"ssn\": \"123-45-6789\",\n \"creditCard\": \"4111111111111111\",\n \"dateOfBirth\": \"1990-01-15\"\n }\n }\n}\n```\n\n```javascript\n// ❌ Returning entire database record\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json(user); // Exposes ALL fields including sensitive ones\n});\n```\n\n**Problems:**\n- Exposed password hashes enable offline brute force attacks\n- API keys in responses allow unauthorized access to third-party services\n- Internal IDs help attackers enumerate resources and map infrastructure\n- PII exposure violates privacy regulations (GDPR, CCPA, PCI-DSS)\n- Returning full database records exposes fields never intended for clients\n\n## Correct\n\n```javascript\n// ✅ Explicit field selection per context\nconst userPublicFields = ['id', 'name', 'avatar', 'createdAt'];\nconst userPrivateFields = [...userPublicFields, 'email', 'settings'];\nconst userAdminFields = [...userPrivateFields, 'roles', 'lastLoginAt', 'status'];\n\nfunction sanitizeUser(user, context = 'public') {\n const allowedFields = {\n public: userPublicFields,\n private: userPrivateFields,\n admin: userAdminFields\n }[context];\n\n return Object.fromEntries(\n Object.entries(user).filter(([key]) => allowedFields.includes(key))\n );\n}\n\n// Use explicit field selection\napp.get('/users/:id', authenticate, async (req, res) => {\n const user = await db.findUser(req.params.id);\n\n if (!user) {\n return res.status(404).json({ error: 'User not found' });\n }\n\n // Determine context based on requester\n let context = 'public';\n if (req.user.id === user.id) {\n context = 'private';\n } else if (req.user.roles.includes('admin')) {\n context = 'admin';\n }\n\n res.json(sanitizeUser(user, context));\n});\n\n// Use DTOs/serializers\nclass UserResponse {\n constructor(user, includePrivate = false) {\n this.id = user.id;\n this.name = user.name;\n this.avatar = user.avatar;\n this.createdAt = user.createdAt;\n\n if (includePrivate) {\n this.email = user.email;\n this.settings = user.settings;\n }\n }\n}\n\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n const includePrivate = req.user?.id === user.id;\n res.json(new UserResponse(user, includePrivate));\n});\n\n// Mask sensitive data\nfunction maskEmail(email) {\n const [local, domain] = email.split('@');\n const maskedLocal = local[0] + '*'.repeat(local.length - 2) + local.slice(-1);\n return `${maskedLocal}@${domain}`;\n}\n\nfunction maskPhone(phone) {\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n}\n\nfunction maskCreditCard(number) {\n return '**** **** **** ' + number.slice(-4);\n}\n\n// Apply masking in responses\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n\n res.json({\n id: user.id,\n name: user.name,\n email: req.user.id === user.id ? user.email : maskEmail(user.email),\n phone: maskPhone(user.phone),\n paymentMethod: {\n type: 'card',\n last4: user.creditCard.slice(-4),\n brand: user.cardBrand\n }\n });\n});\n```\n\n```python\n# ✅ FastAPI with Pydantic response models\nfrom pydantic import BaseModel, EmailStr, Field\nfrom typing import Optional\n\n# Internal model (full data)\nclass UserInternal(BaseModel):\n id: int\n email: str\n password_hash: str\n name: str\n ssn: Optional[str]\n api_key: str\n stripe_customer_id: str\n\n# Public response model (safe to expose)\nclass UserPublic(BaseModel):\n id: int\n name: str\n avatar_url: Optional[str]\n\n class Config:\n # Only include fields defined in this model\n extra = 'forbid'\n\n# Private response model (for account owner)\nclass UserPrivate(UserPublic):\n email: EmailStr\n settings: dict\n\n# Admin response model\nclass UserAdmin(UserPrivate):\n status: str\n roles: list\n last_login_at: Optional[str]\n\[email protected](\"/users/{user_id}\", response_model=UserPublic)\nasync def get_user(user_id: int, current_user = Depends(get_current_user)):\n user = await db.get_user(user_id)\n\n if current_user.id == user_id:\n return UserPrivate(**user.dict())\n elif \"admin\" in current_user.roles:\n return UserAdmin(**user.dict())\n else:\n return UserPublic(**user.dict())\n\n# Mask sensitive data in logs\nimport logging\n\nclass SensitiveDataFilter(logging.Filter):\n PATTERNS = [\n (r'\"password\":\\s*\"[^\"]*\"', '\"password\": \"[REDACTED]\"'),\n (r'\"token\":\\s*\"[^\"]*\"', '\"token\": \"[REDACTED]\"'),\n (r'\"apiKey\":\\s*\"[^\"]*\"', '\"apiKey\": \"[REDACTED]\"'),\n (r'\\b\\d{3}-\\d{2}-\\d{4}\\b', '[SSN REDACTED]'),\n (r'\\b\\d{16}\\b', '[CARD REDACTED]'),\n ]\n\n def filter(self, record):\n import re\n message = record.getMessage()\n for pattern, replacement in self.PATTERNS:\n message = re.sub(pattern, replacement, message)\n record.msg = message\n return True\n```\n\n```json\n// ✅ Safe user response\n{\n \"id\": 123,\n \"name\": \"John Doe\",\n \"avatar\": \"https://cdn.example.com/avatars/123.jpg\",\n \"email\": \"j***[email protected]\",\n \"memberSince\": \"2023-01-15\"\n}\n\n// ✅ Safe payment method response\n{\n \"paymentMethods\": [\n {\n \"id\": \"pm_abc123\",\n \"type\": \"card\",\n \"brand\": \"visa\",\n \"last4\": \"4242\",\n \"expiryMonth\": 12,\n \"expiryYear\": 2025\n }\n ]\n}\n```\n\n## Sensitive Data Checklist\n\n| Field | Action | Reason |\n|-------|--------|--------|\n| Password hash | Never expose | Security |\n| API keys | Never expose | Security |\n| SSN/Tax ID | Never/mask | PII/Compliance |\n| Full credit card | Never expose | PCI-DSS |\n| Internal IDs | Usually hide | Information disclosure |\n| Email (other users) | Mask | Privacy |\n| Phone | Mask | Privacy |\n| Address | Context-dependent | Privacy |\n\n**Benefits:**\n- Prevents account takeover from exposed credentials\n- Meets privacy regulation requirements (GDPR, CCPA, PCI-DSS)\n- Reduces attack surface by hiding internal system identifiers\n- Context-based field exposure follows the principle of least privilege\n- Data masking provides useful info without exposing full values\n- Simplifies compliance audits with clear data handling policies\n\nReference: [OWASP Sensitive Data Exposure](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/09-Testing_for_Weak_Cryptography/04-Testing_for_Weak_Encryption)\n\n\n---\n\n## Cursor-Based Pagination for Large Datasets\n\n**Impact: HIGH (Constant O(1) pagination performance regardless of dataset depth)**\n\nOffset pagination degrades linearly as page depth increases because the database must scan and discard all preceding rows. Cursor-based pagination uses an opaque pointer to the last retrieved item, enabling the database to seek directly to the next batch with constant performance.\n\n## Incorrect\n\n```http\nGET /api/v1/orders?offset=500000&limit=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 500001, \"total\": 49.99 },\n { \"id\": 500002, \"total\": 129.00 }\n ]\n}\n```\n\n```sql\n-- Behind the scenes: database scans 500,000 rows before returning 20\nSELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 500000;\n```\n\n**Problems:**\n- Query time grows linearly with offset — page 25,000 is dramatically slower than page 1\n- Database must scan and discard all rows before the offset\n- Inconsistent results if rows are inserted or deleted between page requests\n- Memory and CPU waste on large tables (millions of rows)\n\n## Correct\n\n```http\nGET /api/v1/orders?cursor=eyJpZCI6NTAwMDAwfQ&limit=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 500001, \"total\": 49.99 },\n { \"id\": 500002, \"total\": 129.00 }\n ],\n \"meta\": {\n \"has_more\": true,\n \"next_cursor\": \"eyJpZCI6NTAwMDIwfQ\"\n },\n \"links\": {\n \"next\": \"/api/v1/orders?cursor=eyJpZCI6NTAwMDIwfQ&limit=20\"\n }\n}\n```\n\n```sql\n-- Behind the scenes: index seek, constant performance\nSELECT * FROM orders WHERE id > 500000 ORDER BY id LIMIT 20;\n```\n\n**Benefits:**\n- Constant query time regardless of how deep into the dataset the client has paginated\n- Stable results — no skipped or duplicated items when data changes between requests\n- Efficient use of database indexes (seek instead of scan)\n- Opaque cursor allows server-side implementation changes without breaking clients\n\nReference: [Slack API - Pagination](https://api.slack.com/docs/pagination)\n\n\n---\n\n## Offset Pagination for Simple Cases\n\n**Impact: HIGH (Enables random page access and predictable navigation for small-to-medium datasets)**\n\nOffset-based pagination is the most intuitive model for clients that need numbered pages, total counts, and the ability to jump to arbitrary pages. It works well for small-to-medium datasets and should always include metadata and navigation links so clients never have to guess the pagination state.\n\n## Incorrect\n\n```http\nGET /api/v1/articles?page=2\n```\n\n```json\n[\n { \"id\": 21, \"title\": \"Introduction to REST\" },\n { \"id\": 22, \"title\": \"API Versioning\" }\n]\n```\n\n**Problems:**\n- Bare array provides no pagination context — client cannot determine total pages or current position\n- No navigation links — client must construct URLs manually and guess when to stop\n- No indication of page size — unclear how many items per page the server returned\n- Client cannot build a page selector UI without total count information\n\n## Correct\n\n```http\nGET /api/v1/articles?page=2&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 21, \"title\": \"Introduction to REST\" },\n { \"id\": 22, \"title\": \"API Versioning\" }\n ],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 20,\n \"total_pages\": 10,\n \"total_count\": 195\n },\n \"links\": {\n \"first\": \"/api/v1/articles?page=1&per_page=20\",\n \"prev\": \"/api/v1/articles?page=1&per_page=20\",\n \"next\": \"/api/v1/articles?page=3&per_page=20\",\n \"last\": \"/api/v1/articles?page=10&per_page=20\"\n }\n}\n```\n\n**Benefits:**\n- Full pagination envelope lets clients render page selectors, \"showing X of Y\" indicators, and navigation controls\n- Navigation links follow HATEOAS principles — clients follow links rather than constructing URLs\n- `prev` and `next` are null-safe (omitted on first and last pages respectively)\n- `per_page` parameter gives clients control over batch size within server-enforced limits\n\nReference: [JSON:API - Pagination](https://jsonapi.org/format/#fetching-pagination)\n\n\n---\n\n## Consistent Pagination Parameter Names\n\n**Impact: HIGH (Reduces client integration time by 40-60% through predictable parameter conventions)**\n\nWhen different endpoints use different parameter names for pagination, every client integration becomes a special case. Developers waste time reading docs for each endpoint instead of applying one convention everywhere. Pick one style and enforce it across the entire API.\n\n## Incorrect\n\n```http\n# Users endpoint uses limit/offset\nGET /api/v1/users?limit=20&offset=40\n\n# Orders endpoint uses page/per_page\nGET /api/v1/orders?page=3&per_page=20\n\n# Products endpoint uses size/number\nGET /api/v1/products?size=20&number=3\n\n# Search endpoint uses count/start\nGET /api/v1/search?count=20&start=40\n```\n\n**Problems:**\n- Client SDKs need endpoint-specific pagination logic instead of a shared helper\n- Developers must consult documentation for every endpoint to find the right parameter names\n- Generic pagination UI components cannot be reused across different resource types\n- Increased chance of bugs when developers assume one convention but the endpoint uses another\n\n## Correct\n\n```http\n# Offset-based: use \"page\" + \"per_page\" everywhere\nGET /api/v1/users?page=3&per_page=20\nGET /api/v1/orders?page=3&per_page=20\nGET /api/v1/products?page=3&per_page=20\n\n# Cursor-based: use \"cursor\" + \"limit\" everywhere\nGET /api/v1/events?cursor=eyJpZCI6MTIzfQ&limit=20\nGET /api/v1/notifications?cursor=eyJpZCI6NDU2fQ&limit=20\nGET /api/v1/logs?cursor=eyJpZCI6Nzg5fQ&limit=20\n```\n\n**Benefits:**\n- One pagination helper in the client SDK handles all endpoints\n- Developers learn the convention once and apply it everywhere\n- Generic UI components (pagers, infinite scroll) work with any resource\n- API documentation is simpler — pagination is explained once, not per-endpoint\n\nReference: [Microsoft REST API Guidelines - Pagination](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#pagination)\n\n\n---\n\n## Include Pagination Metadata in Responses\n\n**Impact: HIGH (Eliminates guesswork pagination and reduces unnecessary API calls by 30-50%)**\n\nWithout pagination metadata, clients must make an extra request to discover there are no more results, or blindly paginate until they receive an empty response. Including metadata in every paginated response gives clients everything they need to render UI controls and make efficient decisions about fetching more data.\n\n## Incorrect\n\n```http\nGET /api/v1/products?page=2&per_page=20\n```\n\n```json\n[\n { \"id\": 21, \"name\": \"Widget A\", \"price\": 9.99 },\n { \"id\": 22, \"name\": \"Widget B\", \"price\": 14.99 }\n]\n```\n\n**Problems:**\n- Client cannot distinguish \"page has fewer items than per_page\" from \"this is the last page\"\n- No total count means page selector UIs and \"X results found\" labels are impossible\n- Client must request the next page to discover it is empty — wasting a round trip\n- No navigation links forces clients to manually construct pagination URLs\n\n## Correct\n\n```http\nGET /api/v1/products?page=2&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 21, \"name\": \"Widget A\", \"price\": 9.99 },\n { \"id\": 22, \"name\": \"Widget B\", \"price\": 14.99 }\n ],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 20,\n \"total_count\": 195,\n \"total_pages\": 10,\n \"has_more\": true\n },\n \"links\": {\n \"first\": \"/api/v1/products?page=1&per_page=20\",\n \"prev\": \"/api/v1/products?page=1&per_page=20\",\n \"next\": \"/api/v1/products?page=3&per_page=20\",\n \"last\": \"/api/v1/products?page=10&per_page=20\"\n }\n}\n```\n\n**Benefits:**\n- `has_more` lets infinite-scroll UIs know when to stop fetching without an extra empty request\n- `total_count` and `total_pages` enable \"Showing 21-40 of 195 results\" display\n- Navigation links let clients follow links instead of constructing URLs, reducing coupling\n- Consistent envelope structure makes client-side deserialization predictable across all endpoints\n\nReference: [GitHub REST API - Pagination](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api)\n\n\n---\n\n## Filter via Query Parameters\n\n**Impact: HIGH (Cacheable, bookmarkable filtering that leverages HTTP semantics correctly)**\n\nFiltering is a read operation and belongs in GET requests with query parameters. Using POST bodies for filtering breaks HTTP cacheability, makes URLs non-shareable, and violates the semantic contract of HTTP methods. Standard query parameters are combinable, cacheable, and immediately understandable.\n\n## Incorrect\n\n```http\nPOST /api/v1/users/search\nContent-Type: application/json\n\n{\n \"filters\": {\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_after\": \"2024-01-01\"\n }\n}\n```\n\n```http\n# Or: custom filter syntax that requires a parser\nGET /api/v1/users?filter=status:eq:active|role:eq:admin|created:gt:2024-01-01\n```\n\n**Problems:**\n- POST for read operations breaks HTTP caching at every layer (CDN, browser, proxy)\n- URLs cannot be bookmarked, shared, or logged meaningfully\n- Custom filter syntax requires client-side query builders and server-side parsers\n- Violates REST semantics — POST implies resource creation or mutation, not retrieval\n\n## Correct\n\n```http\nGET /api/v1/users?status=active&role=admin&created_after=2024-01-01\n```\n\n```json\n{\n \"data\": [\n {\n \"id\": 42,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_at\": \"2024-03-15T10:30:00Z\"\n }\n ],\n \"meta\": {\n \"total_count\": 12,\n \"filters_applied\": {\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_after\": \"2024-01-01\"\n }\n }\n}\n```\n\n```http\n# Multiple values for the same field (OR logic)\nGET /api/v1/users?status=active&status=pending\n\n# Range filters with clear suffixes\nGET /api/v1/orders?total_min=100&total_max=500&created_after=2024-01-01\n\n# Combine with search\nGET /api/v1/users?role=admin&q=smith\n```\n\n**Benefits:**\n- Fully cacheable by CDNs, reverse proxies, and browsers\n- URLs are bookmarkable and shareable — useful for dashboards and saved views\n- Filters are self-documenting and combinable with standard `&` syntax\n- No custom parser needed — standard query string parsing libraries handle it\n\nReference: [Google API Design Guide - Standard Methods](https://cloud.google.com/apis/design/standard_methods)\n\n\n---\n\n## Flexible Sorting Options\n\n**Impact: HIGH (Empowers clients to retrieve data in the exact order they need without server-side changes)**\n\nAPIs that return data in a single hardcoded order force clients to re-sort in memory, which is wasteful and breaks pagination. A flexible sorting API lets clients request the order they need, and the database handles it efficiently using indexes.\n\n## Incorrect\n\n```http\n# No sort parameter — always returns by id ASC\nGET /api/v1/products\n```\n\n```json\n[\n { \"id\": 1, \"name\": \"Alpha\", \"price\": 29.99, \"created_at\": \"2023-01-01T00:00:00Z\" },\n { \"id\": 2, \"name\": \"Beta\", \"price\": 9.99, \"created_at\": \"2024-06-15T00:00:00Z\" }\n]\n```\n\n```http\n# Or: inconsistent sort parameters across endpoints\nGET /api/v1/products?order_by=price&direction=desc\nGET /api/v1/users?sortField=name&sortOrder=asc\n```\n\n**Problems:**\n- Client must fetch all data and sort in memory, defeating the purpose of pagination\n- No way to get \"newest first\" or \"cheapest first\" without client-side processing\n- Inconsistent sort parameter names across endpoints increase integration complexity\n- Hardcoded order may not match any client's primary use case\n\n## Correct\n\n```http\n# Ascending sort (default direction)\nGET /api/v1/products?sort=price\n\n# Descending sort with \"-\" prefix\nGET /api/v1/products?sort=-created_at\n\n# Multiple sort fields (comma-separated)\nGET /api/v1/products?sort=category,-price\n\n# Combined with filtering and pagination\nGET /api/v1/products?status=active&sort=-created_at&page=1&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 47, \"name\": \"New Widget\", \"price\": 59.99, \"created_at\": \"2024-06-15T00:00:00Z\" },\n { \"id\": 32, \"name\": \"Another Widget\", \"price\": 39.99, \"created_at\": \"2024-05-10T00:00:00Z\" }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"per_page\": 20,\n \"total_count\": 84,\n \"sort\": \"-created_at\"\n }\n}\n```\n\n**Benefits:**\n- `-` prefix convention for descending is compact and widely adopted (JSON:API, many major APIs)\n- Multiple sort fields let clients express complex ordering like \"category ascending, then price descending\"\n- Sorting happens at the database level where indexes make it efficient\n- Consistent `sort` parameter name across all endpoints simplifies client SDKs\n\nReference: [JSON:API - Sorting](https://jsonapi.org/format/#fetching-sorting)\n\n\n---\n\n## Version in URL Path\n\n**Impact: HIGH (Explicit version visibility prevents accidental breaking-change consumption)**\n\nURL path versioning is the most explicit and widely adopted approach to API versioning. The version is visible in every request, making it impossible to accidentally hit the wrong version. It works naturally with routing, caching, load balancing, and documentation tools.\n\n## Incorrect\n\n```http\n# No version — all consumers share one contract\nGET /api/users\n```\n\n```json\n// v1 response shape\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```json\n// After a breaking change — same URL, different shape\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"name\": null\n}\n```\n\n**Problems:**\n- Breaking changes immediately affect all consumers with no migration path\n- No way to run old and new versions side-by-side during transition periods\n- Clients cannot pin to a known-good contract — any deploy can break them\n- Rollback requires reverting the entire API, not just routing rules\n\n## Correct\n\n```http\n# Version 1 — original contract\nGET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```http\n# Version 2 — new contract, coexists with v1\nGET /api/v2/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"profile\": {\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n }\n}\n```\n\n**Benefits:**\n- Version is immediately visible in URLs, logs, and documentation\n- Old and new versions run simultaneously — consumers migrate at their own pace\n- Load balancers and API gateways can route versions to different backends\n- CDNs and proxies cache each version independently without conflict\n\nReference: [Stripe API - Versioning](https://stripe.com/docs/api/versioning)\n\n\n---\n\n## Version via Accept Header\n\n**Impact: HIGH (Clean URLs with content negotiation-based versioning for API evolution)**\n\nHeader-based versioning uses the `Accept` header to specify the desired API version, keeping URLs clean and resource-centric. This approach aligns with HTTP content negotiation semantics and is preferred when URL aesthetics matter or when the same resource should be accessible across versions without changing its canonical URL.\n\n## Incorrect\n\n```http\n# No version information at all — client gets whatever the current version is\nGET /api/users/1\nAccept: application/json\n```\n\n```http\n# Or: custom non-standard header\nGET /api/users/1\nX-API-Version: 2\n```\n\n**Problems:**\n- No standard mechanism to request a specific version of the response format\n- Custom headers are not part of HTTP content negotiation and may be stripped by proxies\n- Clients have no guarantee about the response shape they will receive\n- `X-` prefixed headers are deprecated by RFC 6648 and signal non-standard behavior\n\n## Correct\n\n```http\n# Request version 1\nGET /api/users/1\nAccept: application/vnd.myapi.v1+json\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```http\n# Request version 2\nGET /api/users/1\nAccept: application/vnd.myapi.v2+json\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"profile\": {\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n }\n}\n```\n\n```http\n# Server responds with matching Content-Type\nHTTP/1.1 200 OK\nContent-Type: application/vnd.myapi.v2+json\n```\n\n| Aspect | URL Path (`/v1/`) | Accept Header |\n|---|---|---|\n| Visibility | Version in every URL | Hidden in headers |\n| Caching | Simple (URL-based) | Requires `Vary: Accept` |\n| Browser testing | Easy (type URL) | Needs tool (curl, Postman) |\n| URL cleanliness | Extra path segment | Clean resource URLs |\n| HTTP semantics | Convention-based | Proper content negotiation |\n\n**Benefits:**\n- URLs remain clean and resource-focused — `/api/users/1` is the canonical identifier\n- Follows HTTP content negotiation standards (`Accept` / `Content-Type`)\n- Server can default to the latest version when no version header is sent\n- Multiple representations of the same resource at the same URL, which aligns with REST principles\n\nReference: [GitHub API - Media Types](https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#accept)\n\n\n---\n\n## Maintain Backward Compatibility\n\n**Impact: HIGH (Prevents breaking existing integrations and avoids costly emergency client fixes)**\n\nBreaking changes in a live API force every consumer to update simultaneously or face outages. Maintaining backward compatibility within a version means clients continue working after deployments, and new features are delivered through additive changes only. Reserve breaking changes for new major versions.\n\n## Incorrect\n\n```http\n# Before: GET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"role\": \"admin\"\n}\n```\n\n```http\n# After deploy (same v1): field renamed, field removed, type changed\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"roles\": [\"admin\", \"editor\"]\n}\n```\n\n**Problems:**\n- Renaming `name` to `full_name` breaks every client reading `response.name`\n- Renaming `email` to `email_address` breaks form bindings and display logic\n- Changing `role` (string) to `roles` (array) causes type errors in client deserialization\n- Removing fields with no notice gives consumers zero time to adapt\n\n## Correct\n\n```http\n# Additive changes only within v1: new fields added, old fields preserved\nGET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"full_name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"email_address\": \"[email protected]\",\n \"role\": \"admin\",\n \"roles\": [\"admin\", \"editor\"],\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n}\n```\n\n```http\n# Deprecation communicated via response headers\nHTTP/1.1 200 OK\nContent-Type: application/json\nX-Deprecated-Fields: name, email, role\n```\n\n```http\n# New endpoints are always safe to add\nGET /api/v1/users/1/preferences # new endpoint, no existing contract\n```\n\n**Benefits:**\n- Existing clients continue working without any code changes after every deploy\n- New clients can adopt new field names immediately while old names remain available\n- Deprecation headers give automated tooling a way to detect and flag stale usage\n- New endpoints and new fields never conflict with existing client expectations\n\nReference: [Stripe API - Backward Compatibility](https://stripe.com/docs/upgrades)\n\n\n---\n\n## Deprecation Strategy\n\n**Impact: HIGH (Structured deprecation prevents surprise outages and gives consumers a clear migration path)**\n\nRemoving an API version or endpoint without advance notice breaks consumer trust and causes production outages. A proper deprecation strategy uses standard HTTP headers, documented timelines, and migration guides to give consumers the time and information they need to transition safely.\n\n## Incorrect\n\n```http\n# Monday: API v1 is working\nGET /api/v1/users\nHTTP/1.1 200 OK\n\n# Tuesday: API v1 is gone without warning\nGET /api/v1/users\nHTTP/1.1 404 Not Found\n\n{\n \"error\": \"This endpoint no longer exists\"\n}\n```\n\n**Problems:**\n- Consumers discover the removal through production failures, not proactive communication\n- No migration period means every consumer must update simultaneously or break\n- No documentation of what changed or where to go creates confusion and support burden\n- Erodes trust — consumers cannot rely on the API for production workloads\n\n## Correct\n\n```http\n# Phase 1: Announce deprecation (minimum 6 months before removal)\nGET /api/v1/users\nHTTP/1.1 200 OK\nDeprecation: true\nSunset: Sat, 01 Jun 2025 00:00:00 GMT\nLink: \u003c/api/v2/migration-guide>; rel=\"deprecation\"\n```\n\n```json\n{\n \"data\": [\n { \"id\": 1, \"name\": \"Jane Smith\" }\n ],\n \"_deprecation\": {\n \"message\": \"API v1 is deprecated. Please migrate to v2 by June 1, 2025.\",\n \"migration_guide\": \"https://api.example.com/docs/v1-to-v2-migration\",\n \"sunset_date\": \"2025-06-01T00:00:00Z\"\n }\n}\n```\n\n```http\n# Phase 2: After sunset date — return 410 Gone (not 404)\nGET /api/v1/users\nHTTP/1.1 410 Gone\n\n{\n \"error\": {\n \"code\": \"API_VERSION_SUNSET\",\n \"message\": \"API v1 was removed on June 1, 2025. Please use /api/v2/users.\",\n \"migration_guide\": \"https://api.example.com/docs/v1-to-v2-migration\",\n \"current_version\": \"/api/v2/users\"\n }\n}\n```\n\n```\nDeprecation Timeline:\n T-6 months: Add Deprecation + Sunset headers, publish migration guide\n T-3 months: Send email/notification to API consumers\n T-1 month: Final reminder, log consumers still using deprecated version\n T-0: Return 410 Gone with redirect information\n T+3 months: Remove deprecated code from codebase\n```\n\n**Benefits:**\n- `Sunset` header (RFC 8594) is a machine-readable standard that monitoring tools can detect automatically\n- `410 Gone` correctly signals permanent removal (unlike `404` which implies the resource may return)\n- Migration guide link in both headers and body ensures consumers can find upgrade instructions\n- Minimum 6-month window gives teams of all sizes enough time to plan and execute migration\n\nReference: [RFC 8594 - The Sunset HTTP Header Field](https://datatracker.ietf.org/doc/html/rfc8594)\n\n\n---\n\n## Consistent Response Envelope\n\n**Impact: MEDIUM (Reduces client-side parsing complexity by 40-60%)**\n\nA consistent response envelope allows API consumers to build reusable parsing logic that works across every endpoint. Without it, clients must special-case each endpoint's response shape, leading to fragile integration code and slower onboarding.\n\n## Incorrect\n\n```json\n// ❌ Different shapes for different endpoints\n\n// GET /users/123 — bare object\n{\n \"id\": 123,\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n}\n\n// GET /users — bare array\n[\n { \"id\": 123, \"name\": \"Jane Doe\" },\n { \"id\": 456, \"name\": \"John Smith\" }\n]\n\n// GET /orders — nested differently\n{\n \"orders\": [\n { \"id\": 1, \"total\": 99.99 }\n ],\n \"count\": 1\n}\n\n// GET /products/42 — yet another shape\n{\n \"product\": {\n \"id\": 42,\n \"title\": \"Widget\"\n },\n \"status\": \"ok\"\n}\n```\n\n**Problems:**\n- Clients cannot predict the response structure for new endpoints\n- Every endpoint requires unique parsing logic\n- Impossible to build a generic API client or SDK\n- Adding metadata (pagination, rate limits) requires breaking changes\n\n## Correct\n\n### Simple Envelope Style\n\n```json\n// ✅ Single resource — GET /users/123\n{\n \"data\": {\n \"id\": 123,\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n },\n \"meta\": {\n \"request_id\": \"req_abc123\"\n }\n}\n\n// ✅ Collection — GET /users?page=1&per_page=20\n{\n \"data\": [\n { \"id\": 123, \"name\": \"Jane Doe\" },\n { \"id\": 456, \"name\": \"John Smith\" }\n ],\n \"meta\": {\n \"page\": 1,\n \"per_page\": 20,\n \"total\": 142,\n \"total_pages\": 8,\n \"request_id\": \"req_def456\"\n }\n}\n\n// ✅ Empty collection — GET /users?status=banned\n{\n \"data\": [],\n \"meta\": {\n \"page\": 1,\n \"per_page\": 20,\n \"total\": 0,\n \"total_pages\": 0,\n \"request_id\": \"req_ghi789\"\n }\n}\n```\n\n### JSON:API Style\n\n```json\n// ✅ Single resource — GET /users/123\n{\n \"data\": {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": {\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n },\n \"relationships\": {\n \"company\": {\n \"data\": { \"type\": \"companies\", \"id\": \"7\" }\n }\n }\n },\n \"included\": [\n {\n \"type\": \"companies\",\n \"id\": \"7\",\n \"attributes\": {\n \"name\": \"Acme Corp\"\n }\n }\n ]\n}\n\n// ✅ Collection — GET /users\n{\n \"data\": [\n {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": { \"name\": \"Jane Doe\" }\n },\n {\n \"type\": \"users\",\n \"id\": \"456\",\n \"attributes\": { \"name\": \"John Smith\" }\n }\n ],\n \"meta\": {\n \"total\": 142,\n \"page\": 1,\n \"per_page\": 20\n },\n \"links\": {\n \"self\": \"/users?page=1\",\n \"next\": \"/users?page=2\",\n \"last\": \"/users?page=8\"\n }\n}\n```\n\n**Benefits:**\n- Clients build one parser that works for every endpoint\n- Metadata (pagination, request IDs, rate limits) has a predictable location\n- New metadata can be added to `meta` without breaking existing clients\n- SDKs and generic API wrappers become straightforward to implement\n\nReference: [JSON:API Specification](https://jsonapi.org/format/)\n\n\n---\n\n## JSON Naming Conventions\n\n**Impact: MEDIUM (Eliminates field-name guessing and mapping errors)**\n\nInconsistent naming forces developers to guess field names and write tedious mapping code. Picking one convention and applying it everywhere makes the API predictable and reduces integration bugs.\n\n## Incorrect\n\n```json\n// ❌ Mixed conventions in the same response\n{\n \"userId\": 123,\n \"first_name\": \"Jane\",\n \"LastName\": \"Doe\",\n \"Email\": \"[email protected]\",\n \"created_at\": \"2024-01-15\",\n \"lastLogin\": \"Jan 20, 2024 3:45 PM\",\n \"isActive\": true,\n \"acct_type\": \"premium\",\n \"DOB\": \"1990-05-20\",\n \"addr\": {\n \"str\": \"123 Main St\",\n \"ZipCode\": \"90210\"\n }\n}\n```\n\n**Problems:**\n- Developers cannot predict whether a field uses camelCase, snake_case, or PascalCase\n- Abbreviations like `acct`, `str`, `DOB` are ambiguous\n- Date formats vary across fields, requiring per-field parsing\n- Mapping between API responses and client models becomes error-prone\n\n## Correct\n\n### snake_case (common for Ruby, Python, PHP APIs)\n\n```json\n// ✅ Consistent snake_case\n{\n \"user_id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"created_at\": \"2024-01-15T10:30:00Z\",\n \"last_login_at\": \"2024-01-20T15:45:00Z\",\n \"is_active\": true,\n \"account_type\": \"premium\",\n \"date_of_birth\": \"1990-05-20\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"zip_code\": \"90210\"\n }\n}\n```\n\n### camelCase (common for JavaScript/TypeScript APIs)\n\n```json\n// ✅ Consistent camelCase\n{\n \"userId\": 123,\n \"firstName\": \"Jane\",\n \"lastName\": \"Doe\",\n \"email\": \"[email protected]\",\n \"createdAt\": \"2024-01-15T10:30:00Z\",\n \"lastLoginAt\": \"2024-01-20T15:45:00Z\",\n \"isActive\": true,\n \"accountType\": \"premium\",\n \"dateOfBirth\": \"1990-05-20\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"zipCode\": \"90210\"\n }\n}\n```\n\n### Date and Time — Always ISO 8601\n\n```json\n// ✅ ISO 8601 with timezone\n{\n \"created_at\": \"2024-01-15T10:30:00Z\",\n \"updated_at\": \"2024-03-01T14:22:33+05:30\",\n \"expires_on\": \"2024-12-31\",\n \"duration_seconds\": 3600\n}\n\n// ❌ Avoid non-standard date formats\n{\n \"created_at\": \"Jan 15, 2024\",\n \"updated_at\": \"03/01/2024\",\n \"expires_on\": \"1704067200\"\n}\n```\n\n### Null vs Omitted Fields\n\n```json\n// ✅ Use null for \"set but empty\" — include the field\n{\n \"first_name\": \"Jane\",\n \"middle_name\": null,\n \"last_name\": \"Doe\"\n}\n\n// ✅ Omit fields that don't apply to this resource\n// A \"company\" user has a company_name; a \"personal\" user omits it\n{\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"account_type\": \"personal\"\n}\n```\n\n### Boolean Naming\n\n```json\n// ✅ Use is_, has_, can_, should_ prefixes\n{\n \"is_active\": true,\n \"is_verified\": false,\n \"has_two_factor\": true,\n \"can_edit\": false,\n \"should_notify\": true\n}\n\n// ❌ Ambiguous boolean names\n{\n \"active\": true,\n \"verified\": 1,\n \"two_factor\": \"yes\",\n \"edit\": false,\n \"notification\": true\n}\n```\n\n**Benefits:**\n- Developers can predict any field name without checking the docs\n- Automated serialization/deserialization works without custom mappings\n- ISO 8601 dates are natively parseable in every language\n- Boolean prefixes make the type and intent immediately clear\n\nReference: [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml)\n\n\n---\n\n## Field Selection (Sparse Fieldsets)\n\n**Impact: MEDIUM (Reduces payload size by 50-90% for field-heavy resources)**\n\nWhen an endpoint returns 50+ fields but the client only needs 3, the wasted bandwidth slows mobile apps, increases server serialization time, and drives up data transfer costs. Field selection lets clients request only what they need.\n\n## Incorrect\n\n```http\n// ❌ Client only needs name and avatar, but gets everything\nGET /api/users/123\n\n// Response: 2.4 KB\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"phone\": \"+1-555-0100\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\",\n \"date_of_birth\": \"1990-05-20\",\n \"bio\": \"Software engineer with 10 years of experience...\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip_code\": \"62701\",\n \"country\": \"US\"\n },\n \"preferences\": {\n \"language\": \"en\",\n \"timezone\": \"America/Chicago\",\n \"theme\": \"dark\",\n \"notifications\": {\n \"email\": true,\n \"sms\": false,\n \"push\": true\n }\n },\n \"social_links\": {\n \"twitter\": \"https://twitter.com/janedoe\",\n \"linkedin\": \"https://linkedin.com/in/janedoe\",\n \"github\": \"https://github.com/janedoe\"\n },\n \"created_at\": \"2023-01-15T10:30:00Z\",\n \"updated_at\": \"2024-03-01T14:22:33Z\",\n \"last_login_at\": \"2024-03-10T09:15:00Z\"\n }\n}\n```\n\n**Problems:**\n- Wasted bandwidth — client discards 90% of the response\n- Slower responses on mobile or low-bandwidth connections\n- Server serializes and queries for unused data\n- Higher data transfer costs at scale\n\n## Correct\n\n### Query Parameter Approach\n\n```http\n// ✅ Client requests only the fields it needs\nGET /api/users/123?fields=id,first_name,last_name,avatar_url\n\n// Response: 180 bytes (93% smaller)\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\"\n }\n}\n```\n\n### Nested Fields\n\n```http\n// ✅ Dot notation for nested field selection\nGET /api/users/123?fields=id,first_name,address.city,address.country\n\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"address\": {\n \"city\": \"Springfield\",\n \"country\": \"US\"\n }\n }\n}\n```\n\n### Collections with Field Selection\n\n```http\n// ✅ Sparse fieldsets on collections — big savings at scale\nGET /api/users?fields=id,first_name,avatar_url&per_page=50\n\n// Instead of 50 × 2.4 KB = 120 KB\n// Now 50 × 120 bytes = 6 KB\n{\n \"data\": [\n { \"id\": 123, \"first_name\": \"Jane\", \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\" },\n { \"id\": 456, \"first_name\": \"John\", \"avatar_url\": \"https://cdn.example.com/avatars/456.jpg\" }\n ],\n \"meta\": {\n \"total\": 142,\n \"page\": 1,\n \"per_page\": 50\n }\n}\n```\n\n### JSON:API Sparse Fieldsets\n\n```http\n// ✅ JSON:API uses typed field selection\nGET /api/users?fields[users]=first_name,avatar_url&fields[companies]=name\n\n{\n \"data\": [\n {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": {\n \"first_name\": \"Jane\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\"\n },\n \"relationships\": {\n \"company\": { \"data\": { \"type\": \"companies\", \"id\": \"7\" } }\n }\n }\n ],\n \"included\": [\n {\n \"type\": \"companies\",\n \"id\": \"7\",\n \"attributes\": { \"name\": \"Acme Corp\" }\n }\n ]\n}\n```\n\n**Benefits:**\n- Payload size drops 50-90% for typical use cases\n- Faster responses, especially on mobile networks\n- Server can optimize database queries to fetch only requested columns\n- Reduced data transfer costs at high request volumes\n\nReference: [Google API Design Guide — Standard Fields](https://cloud.google.com/apis/design/standard_fields)\n\n\n---\n\n## Response Compression\n\n**Impact: MEDIUM (Reduces JSON payload transfer size by 60-80%)**\n\nJSON is highly compressible because of its repetitive structure (keys, braces, quotes). Enabling compression is one of the simplest performance wins for any API, dramatically reducing transfer times with minimal CPU overhead.\n\n## Incorrect\n\n```http\n// ❌ No compression — client doesn't advertise, server doesn't compress\nGET /api/users?per_page=100 HTTP/1.1\nHost: api.example.com\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 245760\n\n// 240 KB uncompressed JSON transferred over the wire\n{\n \"data\": [\n { \"id\": 1, \"name\": \"Jane Doe\", \"email\": \"[email protected]\", ... },\n { \"id\": 2, \"name\": \"John Smith\", \"email\": \"[email protected]\", ... },\n // ... 98 more records\n ]\n}\n```\n\n**Problems:**\n- 240 KB transferred when 48 KB (gzip) or 38 KB (Brotli) would suffice\n- Slower time-to-first-byte, especially on mobile connections\n- Higher bandwidth costs for both server and client\n- Poor experience for users on metered or slow networks\n\n## Correct\n\n### Client Requests Compression\n\n```http\n// ✅ Client advertises supported encodings\nGET /api/users?per_page=100 HTTP/1.1\nHost: api.example.com\nAccept-Encoding: gzip, br\n```\n\n### Server Responds with Compressed Content\n\n```http\n// ✅ gzip compression — widely supported\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Encoding: gzip\nVary: Accept-Encoding\nContent-Length: 48200\n\n// Same 240 KB JSON, now 48 KB over the wire (80% reduction)\n```\n\n```http\n// ✅ Brotli compression — better ratio for modern clients\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Encoding: br\nVary: Accept-Encoding\nContent-Length: 38400\n\n// Same 240 KB JSON, now 38 KB over the wire (84% reduction)\n```\n\n### Typical Compression Ratios for JSON\n\n```\nPayload Type | Raw | gzip | Brotli | gzip % | Brotli %\n-----------------------+---------+---------+---------+---------+---------\nSmall object (1 KB) | 1 KB | 0.5 KB | 0.4 KB | 50% | 60%\nList of 100 records | 240 KB | 48 KB | 38 KB | 80% | 84%\nLarge nested response | 1.2 MB | 180 KB | 140 KB | 85% | 88%\nPaginated collection | 500 KB | 85 KB | 65 KB | 83% | 87%\n```\n\n### Important Headers\n\n```http\n// ✅ Always include Vary header so caches store compressed\n// and uncompressed versions separately\nVary: Accept-Encoding\n\n// ✅ Set minimum size threshold — don't compress tiny responses\n// Most servers skip compression for responses under 1 KB\n```\n\n### Nginx Configuration Example\n\n```nginx\n# Enable gzip compression\ngzip on;\ngzip_types application/json application/javascript text/plain;\ngzip_min_length 1024;\ngzip_comp_level 6;\ngzip_vary on;\n\n# Enable Brotli (if module installed)\nbrotli on;\nbrotli_types application/json application/javascript text/plain;\nbrotli_min_length 1024;\nbrotli_comp_level 6;\n```\n\n### curl — Verifying Compression\n\n```bash\n# Request with compression and see headers\ncurl -s -H \"Accept-Encoding: gzip, br\" \\\n -D - -o /dev/null \\\n https://api.example.com/users\n\n# Decompress and inspect\ncurl -s -H \"Accept-Encoding: gzip\" \\\n --compressed \\\n https://api.example.com/users | jq .\n```\n\n**Benefits:**\n- 60-80% bandwidth reduction with gzip, 70-88% with Brotli\n- Faster response delivery, especially over high-latency connections\n- Lower bandwidth costs at scale\n- Negligible CPU overhead on modern hardware (1-2% for gzip level 6)\n- Transparent to clients — `curl --compressed` and all HTTP libraries handle it automatically\n\nReference: [MDN — Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding)\n\n\n---\n\n## OpenAPI/Swagger Specification\n\n**Impact: MEDIUM (Cuts API integration time by 50% with machine-readable docs)**\n\nAn OpenAPI specification serves as both documentation and a contract. It enables auto-generated client SDKs, interactive docs, and automated testing. Without it, developers rely on outdated wikis or reverse-engineering API behavior from code.\n\n## Incorrect\n\n```\n// ❌ No formal API documentation\n\n// Option A: Nothing at all — developers read source code\n// Option B: Outdated wiki page last edited 18 months ago\n// Option C: Slack messages like \"hey how does the /users endpoint work?\"\n// Option D: README with a few curl examples that no longer match the API\n```\n\n**Problems:**\n- Developers spend hours discovering endpoints and request formats by trial and error\n- No single source of truth — information scattered across wikis, Slack, and code comments\n- Documentation drifts out of sync with the actual API\n- Cannot auto-generate client SDKs, mock servers, or test suites\n\n## Correct\n\n### Minimal OpenAPI 3.1 Specification\n\n```yaml\n# ✅ openapi.yaml — machine-readable, always up to date\nopenapi: \"3.1.0\"\ninfo:\n title: User Management API\n version: \"1.2.0\"\n description: API for managing user accounts\n contact:\n name: API Support\n email: [email protected]\n\nservers:\n - url: https://api.example.com/v1\n description: Production\n - url: https://staging-api.example.com/v1\n description: Staging\n\nsecurity:\n - bearerAuth: []\n\npaths:\n /users:\n get:\n summary: List users\n operationId: listUsers\n tags:\n - Users\n parameters:\n - name: page\n in: query\n schema:\n type: integer\n default: 1\n - name: per_page\n in: query\n schema:\n type: integer\n default: 20\n maximum: 100\n - name: status\n in: query\n schema:\n type: string\n enum: [active, inactive, suspended]\n responses:\n \"200\":\n description: A paginated list of users\n content:\n application/json:\n schema:\n type: object\n properties:\n data:\n type: array\n items:\n $ref: \"#/components/schemas/User\"\n meta:\n $ref: \"#/components/schemas/PaginationMeta\"\n example:\n data:\n - id: 123\n first_name: Jane\n last_name: Doe\n email: [email protected]\n status: active\n created_at: \"2024-01-15T10:30:00Z\"\n meta:\n page: 1\n per_page: 20\n total: 142\n total_pages: 8\n \"401\":\n $ref: \"#/components/responses/Unauthorized\"\n\n post:\n summary: Create a user\n operationId: createUser\n tags:\n - Users\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/CreateUserRequest\"\n example:\n first_name: Jane\n last_name: Doe\n email: [email protected]\n password: secureP@ssw0rd\n responses:\n \"201\":\n description: User created successfully\n content:\n application/json:\n schema:\n type: object\n properties:\n data:\n $ref: \"#/components/schemas/User\"\n \"422\":\n $ref: \"#/components/responses/ValidationError\"\n\ncomponents:\n securitySchemes:\n bearerAuth:\n type: http\n scheme: bearer\n bearerFormat: JWT\n\n schemas:\n User:\n type: object\n properties:\n id:\n type: integer\n first_name:\n type: string\n last_name:\n type: string\n email:\n type: string\n format: email\n status:\n type: string\n enum: [active, inactive, suspended]\n created_at:\n type: string\n format: date-time\n required:\n - id\n - first_name\n - last_name\n - email\n - status\n\n CreateUserRequest:\n type: object\n properties:\n first_name:\n type: string\n minLength: 1\n maxLength: 100\n last_name:\n type: string\n minLength: 1\n maxLength: 100\n email:\n type: string\n format: email\n password:\n type: string\n minLength: 8\n required:\n - first_name\n - last_name\n - email\n - password\n\n PaginationMeta:\n type: object\n properties:\n page:\n type: integer\n per_page:\n type: integer\n total:\n type: integer\n total_pages:\n type: integer\n\n responses:\n Unauthorized:\n description: Authentication required\n content:\n application/json:\n schema:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n example: UNAUTHORIZED\n message:\n type: string\n example: Authentication required\n\n ValidationError:\n description: Request validation failed\n content:\n application/json:\n schema:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n example: VALIDATION_ERROR\n message:\n type: string\n details:\n type: array\n items:\n type: object\n properties:\n field:\n type: string\n code:\n type: string\n message:\n type: string\n```\n\n### Recommended Tools\n\n```\nSwagger UI — Interactive docs with \"Try it out\" button\nRedoc — Clean, responsive three-panel documentation\nStoplight — Visual OpenAPI editor with linting\nSpectral — OpenAPI linting and validation CLI\nopenapi-generator — Auto-generate client SDKs in 40+ languages\n```\n\n**Benefits:**\n- Single source of truth that stays in sync with the codebase\n- Auto-generate client SDKs, mock servers, and test stubs\n- Interactive documentation lets developers test endpoints in the browser\n- CI/CD can validate the spec to catch breaking changes before deploy\n\nReference: [OpenAPI Specification 3.1](https://spec.openapis.org/oas/v3.1.0)\n\n\n---\n\n## Request/Response Examples\n\n**Impact: MEDIUM (Reduces time-to-first-successful-call from hours to minutes)**\n\nSchema definitions tell developers what is possible, but examples show them what to actually do. A developer can copy a curl command, run it, and have a working integration in minutes instead of interpreting abstract schemas for hours.\n\n## Incorrect\n\n```\n// ❌ Documentation with only schema definitions, no examples\n\nPOST /users\n Request Body: CreateUserRequest schema\n Response: User schema (201) | Error schema (422)\n\nParameters:\n first_name: string, required\n last_name: string, required\n email: string, required, format: email\n password: string, required, minLength: 8\n\n// Developer must guess: What does a real request look like?\n// What headers are needed? What does the response actually contain?\n// What does a validation error look like?\n```\n\n**Problems:**\n- Developers must mentally construct requests from abstract schema descriptions\n- No way to quickly copy-paste and test an endpoint\n- Error response shapes are a mystery until they happen in production\n- Onboarding new API consumers takes hours instead of minutes\n\n## Correct\n\n### Complete Request Example\n\n```bash\n# ✅ Create a user — complete curl example\ncurl -X POST https://api.example.com/v1/users \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Accept: application/json\" \\\n -d '{\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"password\": \"secureP@ssw0rd\"\n }'\n```\n\n### Success Response Example\n\n```http\nHTTP/1.1 201 Created\nContent-Type: application/json\nLocation: /v1/users/123\n```\n\n```json\n// ✅ 201 Created — show exact response body\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"status\": \"active\",\n \"created_at\": \"2024-03-15T10:30:00Z\"\n },\n \"meta\": {\n \"request_id\": \"req_abc123\"\n }\n}\n```\n\n### Validation Error Example\n\n```bash\n# ✅ Show what happens with invalid input\ncurl -X POST https://api.example.com/v1/users \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"first_name\": \"\",\n \"email\": \"not-an-email\",\n \"password\": \"short\"\n }'\n```\n\n```json\n// ✅ 422 Unprocessable Entity — show exact error shape\n{\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"The request contains invalid data\",\n \"details\": [\n {\n \"field\": \"first_name\",\n \"code\": \"REQUIRED\",\n \"message\": \"First name is required\"\n },\n {\n \"field\": \"last_name\",\n \"code\": \"REQUIRED\",\n \"message\": \"Last name is required\"\n },\n {\n \"field\": \"email\",\n \"code\": \"INVALID_FORMAT\",\n \"message\": \"Please provide a valid email address\"\n },\n {\n \"field\": \"password\",\n \"code\": \"TOO_SHORT\",\n \"message\": \"Password must be at least 8 characters\"\n }\n ],\n \"request_id\": \"req_def456\"\n }\n}\n```\n\n### Authentication Error Example\n\n```bash\n# ✅ Show what happens with missing/invalid token\ncurl -X GET https://api.example.com/v1/users \\\n -H \"Content-Type: application/json\"\n```\n\n```json\n// ✅ 401 Unauthorized\n{\n \"error\": {\n \"code\": \"UNAUTHORIZED\",\n \"message\": \"Authentication required. Provide a valid Bearer token.\",\n \"request_id\": \"req_ghi789\"\n }\n}\n```\n\n### List Endpoint Example\n\n```bash\n# ✅ GET with query parameters\ncurl -X GET \"https://api.example.com/v1/users?status=active&page=2&per_page=10\" \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Accept: application/json\"\n```\n\n```json\n// ✅ 200 OK — show pagination metadata\n{\n \"data\": [\n {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"status\": \"active\"\n },\n {\n \"id\": 456,\n \"first_name\": \"John\",\n \"last_name\": \"Smith\",\n \"email\": \"[email protected]\",\n \"status\": \"active\"\n }\n ],\n \"meta\": {\n \"page\": 2,\n \"per_page\": 10,\n \"total\": 142,\n \"total_pages\": 15,\n \"request_id\": \"req_jkl012\"\n }\n}\n```\n\n### Documentation Checklist\n\n```\nEvery endpoint should include:\n ✅ Complete curl command with headers and body\n ✅ At least one success response (with realistic data)\n ✅ At least one error response (validation or auth)\n ✅ Query parameter examples for GET endpoints\n ✅ Response headers when relevant (Location, Link, etc.)\n```\n\n**Benefits:**\n- Developers copy-paste and have a working request in seconds\n- Error examples set correct expectations for client-side error handling\n- Realistic data in examples clarifies field formats and semantics\n- Reduces support tickets from confused API consumers\n\nReference: [Stripe API Documentation](https://stripe.com/docs/api) — gold standard for API examples\n\n\n---\n\n## API Changelog\n\n**Impact: MEDIUM (Prevents production outages from undiscovered breaking changes)**\n\nWithout a changelog, API consumers discover breaking changes when their production integrations fail. A dated, categorized changelog gives consumers advance notice of changes so they can adapt their code before it breaks.\n\n## Incorrect\n\n```\n// ❌ No changelog at all\n\n// Consumers discover changes through:\n// - Production errors after a deploy\n// - Slack messages: \"Did something change with /users?\"\n// - Trial and error comparing old vs new behavior\n// - Reading git commit history (if the repo is even public)\n\n// ❌ Or: a vague changelog with no useful detail\n\n## Updates\n- Fixed some bugs\n- Improved performance\n- Updated user endpoint\n```\n\n**Problems:**\n- Breaking changes surprise consumers in production\n- No way to know when a field was deprecated or removed\n- Consumers cannot plan migration timelines for breaking changes\n- Support teams fielding questions that a changelog would answer\n\n## Correct\n\n### Keep a Changelog Format\n\n```markdown\n# API Changelog\n\nAll notable changes to the API are documented here.\nFormat follows [Keep a Changelog](https://keepachangelog.com/).\n\n## [1.5.0] - 2024-03-15\n\n### Added\n- `GET /users` now supports `?sort=created_at` and `?sort=last_login_at`\n query parameters for sorting results\n- New `phone_verified` boolean field on User resource\n- `POST /users/bulk` endpoint for creating up to 100 users in one request\n\n### Changed\n- `GET /users` default `per_page` changed from 50 to 20 for better\n performance. Use `?per_page=50` to restore previous behavior.\n\n### Deprecated\n- `GET /users` query parameter `?order` is deprecated in favor of `?sort`.\n `?order` will be removed in v2.0.0 (scheduled for 2024-09-01).\n- User field `username` is deprecated. Use `email` as the unique identifier.\n Field will be removed in v2.0.0.\n\n## [1.4.2] - 2024-02-28\n\n### Fixed\n- `PATCH /users/:id` now correctly returns 422 instead of 500 when email\n format is invalid\n- Pagination `total_pages` calculation was off by one for exact multiples\n\n## [1.4.1] - 2024-02-10\n\n### Security\n- Rate limiting on `POST /auth/login` reduced from 60 to 10 requests per\n minute to mitigate brute-force attacks\n\n## [1.4.0] - 2024-01-20\n\n### Added\n- `GET /users/:id/activity` endpoint returning recent account activity\n- Support for `fields` query parameter on all GET endpoints (sparse\n fieldsets)\n- `X-Request-Id` response header on all endpoints\n\n### Removed\n- **BREAKING:** `GET /users/search` endpoint removed. Use\n `GET /users?q=search_term` instead. See [migration guide](https://docs.example.com/migration/search).\n\n## [1.3.0] - 2024-01-05\n\n### Added\n- `Accept-Encoding: br` (Brotli) compression support\n- `suspend` and `reactivate` actions on `POST /users/:id/actions`\n\n### Changed\n- Error responses now include `request_id` field in the error envelope\n```\n\n### Changelog Entry Categories\n\n```\nAdded — New endpoints, fields, query parameters, features\nChanged — Non-breaking changes to existing behavior\nDeprecated — Features that will be removed in a future version\nRemoved — BREAKING: features removed in this version\nFixed — Bug fixes\nSecurity — Security-related changes\n```\n\n### Communicating Breaking Changes\n\n```http\n// ✅ Deprecation header on responses for deprecated features\nHTTP/1.1 200 OK\nDeprecation: Sun, 01 Sep 2024 00:00:00 GMT\nSunset: Sun, 01 Sep 2024 00:00:00 GMT\nLink: \u003chttps://docs.example.com/migration/search>; rel=\"deprecation\"\n```\n\n```json\n// ✅ Deprecation notice in response body\n{\n \"data\": { ... },\n \"meta\": {\n \"warnings\": [\n {\n \"code\": \"DEPRECATED_FIELD\",\n \"message\": \"Field 'username' is deprecated and will be removed on 2024-09-01. Use 'email' instead.\",\n \"see\": \"https://docs.example.com/migration/username\"\n }\n ]\n }\n}\n```\n\n### Best Practices\n\n```\n✅ Date every entry (ISO 8601: YYYY-MM-DD)\n✅ Link changelog from your API documentation landing page\n✅ Tag breaking changes with \"BREAKING:\" prefix\n✅ Include migration guides for breaking changes\n✅ Notify consumers via email/webhook before breaking changes\n✅ Give at least 6 months notice before removing deprecated features\n✅ Version the changelog alongside the API (same repo, same release)\n```\n\n**Benefits:**\n- Consumers can review changes before updating their integrations\n- Breaking changes come with advance notice and migration guides\n- Deprecation timelines let consumers plan upgrades\n- Reduces support burden by answering \"what changed?\" proactively\n\nReference: [Keep a Changelog](https://keepachangelog.com/)\n\n\n---\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":194995,"content_sha256":"bbcd63b8abe7bd5b28c3afdf3defd97fea8018f79d6fafd1674c44b3c9c5b47d"},{"filename":"metadata.json","content":"{\n \"name\": \"API Design Patterns\",\n \"version\": \"2.0.0\",\n \"description\": \"RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation.\",\n \"license\": \"MIT\",\n \"author\": {\n \"name\": \"Agent Skills Contributors\",\n \"url\": \"https://github.com/AsyrafHussin/agent-skills\"\n },\n \"categories\": [\n {\n \"name\": \"Resource Design\",\n \"prefix\": \"rest\",\n \"impact\": \"CRITICAL\",\n \"ruleCount\": 8\n },\n {\n \"name\": \"Error Handling\",\n \"prefix\": \"error\",\n \"impact\": \"CRITICAL\",\n \"ruleCount\": 6\n },\n {\n \"name\": \"Security\",\n \"prefix\": \"sec\",\n \"impact\": \"CRITICAL\",\n \"ruleCount\": 7\n },\n {\n \"name\": \"Pagination & Filtering\",\n \"prefix\": \"page/filter/sort\",\n \"impact\": \"HIGH\",\n \"ruleCount\": 6\n },\n {\n \"name\": \"Versioning\",\n \"prefix\": \"ver\",\n \"impact\": \"HIGH\",\n \"ruleCount\": 4\n },\n {\n \"name\": \"Response Format\",\n \"prefix\": \"resp\",\n \"impact\": \"MEDIUM\",\n \"ruleCount\": 4\n },\n {\n \"name\": \"Documentation\",\n \"prefix\": \"doc\",\n \"impact\": \"MEDIUM\",\n \"ruleCount\": 3\n }\n ],\n \"references\": [\n \"https://restfulapi.net\",\n \"https://zalando.github.io/restful-api-guidelines\",\n \"https://github.com/microsoft/api-guidelines\",\n \"https://cloud.google.com/apis/design\",\n \"https://swagger.io/specification\"\n ],\n \"keywords\": [\n \"api\",\n \"rest\",\n \"restful\",\n \"http\",\n \"error-handling\",\n \"pagination\",\n \"versioning\",\n \"security\",\n \"openapi\",\n \"swagger\"\n ],\n \"rulesTotal\": 38,\n \"lastUpdated\": \"2026-03-14\"\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":1759,"content_sha256":"3bfe7d499ef1a109f2b19070fefa90be731c4906150fbb2b72cc07c39c2db5a6"},{"filename":"README.md","content":"# API Design Patterns v2.0.0\n\nRESTful API design principles for building consistent, developer-friendly APIs.\n\n## Overview\n\n- Resource design with proper HTTP methods and status codes\n- Consistent error handling with machine-readable codes\n- Security (authentication, authorization, rate limiting, CORS)\n- Cursor and offset pagination with filtering and sorting\n- API versioning strategies and deprecation\n- Response format conventions and compression\n- OpenAPI documentation and changelog\n- 38 rules across 7 categories\n\n## Categories\n\n### 1. Resource Design (Critical)\nNouns over verbs, plural resources, proper nesting, HTTP methods, status codes, idempotency, HATEOAS.\n\n### 2. Error Handling (Critical)\nConsistent error format, meaningful messages, validation details, error codes, request IDs.\n\n### 3. Security (Critical)\nAuthentication (OAuth2/JWT), authorization (RBAC), rate limiting, input validation, CORS, HTTPS.\n\n### 4. Pagination & Filtering (High)\nCursor-based and offset pagination, consistent parameters, filtering, sorting.\n\n### 5. Versioning (High)\nURL path versioning, header versioning, backward compatibility, deprecation strategy.\n\n### 6. Response Format (Medium)\nConsistent envelope, JSON naming conventions, sparse fieldsets, compression.\n\n### 7. Documentation (Medium)\nOpenAPI/Swagger specification, request/response examples, API changelog.\n\n## Usage\n\n```\nReview my API design\nCheck REST best practices for these endpoints\nDesign error responses for my API\nSet up pagination for this endpoint\n```\n\n## References\n\n- [RESTful API Guidelines](https://restfulapi.net)\n- [Zalando RESTful API Guidelines](https://zalando.github.io/restful-api-guidelines)\n- [Microsoft API Guidelines](https://github.com/microsoft/api-guidelines)\n- [OpenAPI Specification](https://swagger.io/specification)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1809,"content_sha256":"5c73a2afe9b6825f5b201864de7b0290883c6173001d3f8958db1ffc2ea7c3cf"},{"filename":"rules/_sections.md","content":"# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Resource Design (rest)\n\n**Impact:** CRITICAL\n**Description:** Foundational REST principles for API endpoint design. Proper resource naming with nouns, plural collections, correct HTTP method semantics, appropriate status codes, idempotency, and HATEOAS links ensure APIs are intuitive, predictable, and follow industry standards.\n\n## 2. Error Handling (error)\n\n**Impact:** CRITICAL\n**Description:** Consistent error response format across all endpoints. Machine-readable error codes, field-level validation details, meaningful messages, request IDs for debugging, and never exposing stack traces in production enable clients to handle errors programmatically.\n\n## 3. Security (sec)\n\n**Impact:** CRITICAL\n**Description:** API security fundamentals. Authentication (OAuth2/JWT), authorization (RBAC), rate limiting, input validation and sanitization, CORS configuration with whitelists, HTTPS enforcement, and sensitive data protection prevent unauthorized access and common attack vectors.\n\n## 4. Pagination & Filtering (page, filter, sort)\n\n**Impact:** HIGH\n**Description:** Efficient data retrieval for collections. Cursor pagination for large datasets, offset pagination for simple cases, consistent parameter naming, pagination metadata in responses, query parameter filtering, and flexible sorting enable clients to efficiently navigate large datasets.\n\n## 5. Versioning (ver)\n\n**Impact:** HIGH\n**Description:** API versioning strategies for evolving APIs without breaking existing consumers. URL path versioning, header-based versioning, backward compatibility rules, and deprecation strategy with Sunset headers ensure smooth API evolution.\n\n## 6. Response Format (resp)\n\n**Impact:** MEDIUM\n**Description:** Consistent response structure and conventions. Response envelopes, JSON naming conventions (camelCase vs snake_case), sparse fieldsets for bandwidth optimization, and response compression reduce payload sizes and improve developer experience.\n\n## 7. Documentation (doc)\n\n**Impact:** MEDIUM\n**Description:** API documentation standards. OpenAPI/Swagger specifications, complete request/response examples, and API changelogs ensure consumers can discover, understand, and track changes to your API.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2392,"content_sha256":"ebc678cea36af9ec9083f13085b67b4833ac34cf96966a5ece93b7d611c12dc1"},{"filename":"rules/_template.md","content":"---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: Optional description of impact\ntags: tag1, tag2, tag3\n---\n\n## Rule Title Here\n\n**Impact: MEDIUM (optional impact description)**\n\nBrief explanation of the rule and why it matters for API design. Focus on the benefits and problems it solves.\n\n**Incorrect (description of what's wrong):**\n\n```json\n// Bad example showing the anti-pattern\n{\n \"example\": \"bad\"\n}\n```\n\n**Correct (description of what's right):**\n\n```json\n// Good example showing the recommended pattern\n{\n \"example\": \"good\"\n}\n```\n\n```javascript\n// Additional code examples if needed\napp.get('/endpoint', (req, res) => {\n // Implementation\n});\n```\n\n## Why\n\n1. **Benefit 1**: Explanation of why this matters\n2. **Benefit 2**: Another important reason\n3. **Benefit 3**: Additional context\n\nReference: [Link to documentation](https://example.com)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":866,"content_sha256":"daebfcc8468a54c780a4ce952c8d94a32930fd9531b99f49f48c51cff331a3f9"},{"filename":"rules/doc-changelog.md","content":"---\ntitle: API Changelog\nimpact: MEDIUM\nimpactDescription: \"Prevents production outages from undiscovered breaking changes\"\ntags: documentation, changelog, versioning, breaking-changes\n---\n\n## API Changelog\n\n**Impact: MEDIUM (Prevents production outages from undiscovered breaking changes)**\n\nWithout a changelog, API consumers discover breaking changes when their production integrations fail. A dated, categorized changelog gives consumers advance notice of changes so they can adapt their code before it breaks.\n\n## Incorrect\n\n```\n// ❌ No changelog at all\n\n// Consumers discover changes through:\n// - Production errors after a deploy\n// - Slack messages: \"Did something change with /users?\"\n// - Trial and error comparing old vs new behavior\n// - Reading git commit history (if the repo is even public)\n\n// ❌ Or: a vague changelog with no useful detail\n\n## Updates\n- Fixed some bugs\n- Improved performance\n- Updated user endpoint\n```\n\n**Problems:**\n- Breaking changes surprise consumers in production\n- No way to know when a field was deprecated or removed\n- Consumers cannot plan migration timelines for breaking changes\n- Support teams fielding questions that a changelog would answer\n\n## Correct\n\n### Keep a Changelog Format\n\n```markdown\n# API Changelog\n\nAll notable changes to the API are documented here.\nFormat follows [Keep a Changelog](https://keepachangelog.com/).\n\n## [1.5.0] - 2024-03-15\n\n### Added\n- `GET /users` now supports `?sort=created_at` and `?sort=last_login_at`\n query parameters for sorting results\n- New `phone_verified` boolean field on User resource\n- `POST /users/bulk` endpoint for creating up to 100 users in one request\n\n### Changed\n- `GET /users` default `per_page` changed from 50 to 20 for better\n performance. Use `?per_page=50` to restore previous behavior.\n\n### Deprecated\n- `GET /users` query parameter `?order` is deprecated in favor of `?sort`.\n `?order` will be removed in v2.0.0 (scheduled for 2024-09-01).\n- User field `username` is deprecated. Use `email` as the unique identifier.\n Field will be removed in v2.0.0.\n\n## [1.4.2] - 2024-02-28\n\n### Fixed\n- `PATCH /users/:id` now correctly returns 422 instead of 500 when email\n format is invalid\n- Pagination `total_pages` calculation was off by one for exact multiples\n\n## [1.4.1] - 2024-02-10\n\n### Security\n- Rate limiting on `POST /auth/login` reduced from 60 to 10 requests per\n minute to mitigate brute-force attacks\n\n## [1.4.0] - 2024-01-20\n\n### Added\n- `GET /users/:id/activity` endpoint returning recent account activity\n- Support for `fields` query parameter on all GET endpoints (sparse\n fieldsets)\n- `X-Request-Id` response header on all endpoints\n\n### Removed\n- **BREAKING:** `GET /users/search` endpoint removed. Use\n `GET /users?q=search_term` instead. See [migration guide](https://docs.example.com/migration/search).\n\n## [1.3.0] - 2024-01-05\n\n### Added\n- `Accept-Encoding: br` (Brotli) compression support\n- `suspend` and `reactivate` actions on `POST /users/:id/actions`\n\n### Changed\n- Error responses now include `request_id` field in the error envelope\n```\n\n### Changelog Entry Categories\n\n```\nAdded — New endpoints, fields, query parameters, features\nChanged — Non-breaking changes to existing behavior\nDeprecated — Features that will be removed in a future version\nRemoved — BREAKING: features removed in this version\nFixed — Bug fixes\nSecurity — Security-related changes\n```\n\n### Communicating Breaking Changes\n\n```http\n// ✅ Deprecation header on responses for deprecated features\nHTTP/1.1 200 OK\nDeprecation: Sun, 01 Sep 2024 00:00:00 GMT\nSunset: Sun, 01 Sep 2024 00:00:00 GMT\nLink: \u003chttps://docs.example.com/migration/search>; rel=\"deprecation\"\n```\n\n```json\n// ✅ Deprecation notice in response body\n{\n \"data\": { ... },\n \"meta\": {\n \"warnings\": [\n {\n \"code\": \"DEPRECATED_FIELD\",\n \"message\": \"Field 'username' is deprecated and will be removed on 2024-09-01. Use 'email' instead.\",\n \"see\": \"https://docs.example.com/migration/username\"\n }\n ]\n }\n}\n```\n\n### Best Practices\n\n```\n✅ Date every entry (ISO 8601: YYYY-MM-DD)\n✅ Link changelog from your API documentation landing page\n✅ Tag breaking changes with \"BREAKING:\" prefix\n✅ Include migration guides for breaking changes\n✅ Notify consumers via email/webhook before breaking changes\n✅ Give at least 6 months notice before removing deprecated features\n✅ Version the changelog alongside the API (same repo, same release)\n```\n\n**Benefits:**\n- Consumers can review changes before updating their integrations\n- Breaking changes come with advance notice and migration guides\n- Deprecation timelines let consumers plan upgrades\n- Reduces support burden by answering \"what changed?\" proactively\n\nReference: [Keep a Changelog](https://keepachangelog.com/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4816,"content_sha256":"c1508645062e238eb35b1cebd71c0570d902dfcf1fb2539d8deb0e19cf4ed5fd"},{"filename":"rules/doc-examples.md","content":"---\ntitle: Request/Response Examples\nimpact: MEDIUM\nimpactDescription: \"Reduces time-to-first-successful-call from hours to minutes\"\ntags: documentation, examples, curl, requests\n---\n\n## Request/Response Examples\n\n**Impact: MEDIUM (Reduces time-to-first-successful-call from hours to minutes)**\n\nSchema definitions tell developers what is possible, but examples show them what to actually do. A developer can copy a curl command, run it, and have a working integration in minutes instead of interpreting abstract schemas for hours.\n\n## Incorrect\n\n```\n// ❌ Documentation with only schema definitions, no examples\n\nPOST /users\n Request Body: CreateUserRequest schema\n Response: User schema (201) | Error schema (422)\n\nParameters:\n first_name: string, required\n last_name: string, required\n email: string, required, format: email\n password: string, required, minLength: 8\n\n// Developer must guess: What does a real request look like?\n// What headers are needed? What does the response actually contain?\n// What does a validation error look like?\n```\n\n**Problems:**\n- Developers must mentally construct requests from abstract schema descriptions\n- No way to quickly copy-paste and test an endpoint\n- Error response shapes are a mystery until they happen in production\n- Onboarding new API consumers takes hours instead of minutes\n\n## Correct\n\n### Complete Request Example\n\n```bash\n# ✅ Create a user — complete curl example\ncurl -X POST https://api.example.com/v1/users \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Accept: application/json\" \\\n -d '{\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"password\": \"secureP@ssw0rd\"\n }'\n```\n\n### Success Response Example\n\n```http\nHTTP/1.1 201 Created\nContent-Type: application/json\nLocation: /v1/users/123\n```\n\n```json\n// ✅ 201 Created — show exact response body\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"status\": \"active\",\n \"created_at\": \"2024-03-15T10:30:00Z\"\n },\n \"meta\": {\n \"request_id\": \"req_abc123\"\n }\n}\n```\n\n### Validation Error Example\n\n```bash\n# ✅ Show what happens with invalid input\ncurl -X POST https://api.example.com/v1/users \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"first_name\": \"\",\n \"email\": \"not-an-email\",\n \"password\": \"short\"\n }'\n```\n\n```json\n// ✅ 422 Unprocessable Entity — show exact error shape\n{\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"The request contains invalid data\",\n \"details\": [\n {\n \"field\": \"first_name\",\n \"code\": \"REQUIRED\",\n \"message\": \"First name is required\"\n },\n {\n \"field\": \"last_name\",\n \"code\": \"REQUIRED\",\n \"message\": \"Last name is required\"\n },\n {\n \"field\": \"email\",\n \"code\": \"INVALID_FORMAT\",\n \"message\": \"Please provide a valid email address\"\n },\n {\n \"field\": \"password\",\n \"code\": \"TOO_SHORT\",\n \"message\": \"Password must be at least 8 characters\"\n }\n ],\n \"request_id\": \"req_def456\"\n }\n}\n```\n\n### Authentication Error Example\n\n```bash\n# ✅ Show what happens with missing/invalid token\ncurl -X GET https://api.example.com/v1/users \\\n -H \"Content-Type: application/json\"\n```\n\n```json\n// ✅ 401 Unauthorized\n{\n \"error\": {\n \"code\": \"UNAUTHORIZED\",\n \"message\": \"Authentication required. Provide a valid Bearer token.\",\n \"request_id\": \"req_ghi789\"\n }\n}\n```\n\n### List Endpoint Example\n\n```bash\n# ✅ GET with query parameters\ncurl -X GET \"https://api.example.com/v1/users?status=active&page=2&per_page=10\" \\\n -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\" \\\n -H \"Accept: application/json\"\n```\n\n```json\n// ✅ 200 OK — show pagination metadata\n{\n \"data\": [\n {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"status\": \"active\"\n },\n {\n \"id\": 456,\n \"first_name\": \"John\",\n \"last_name\": \"Smith\",\n \"email\": \"[email protected]\",\n \"status\": \"active\"\n }\n ],\n \"meta\": {\n \"page\": 2,\n \"per_page\": 10,\n \"total\": 142,\n \"total_pages\": 15,\n \"request_id\": \"req_jkl012\"\n }\n}\n```\n\n### Documentation Checklist\n\n```\nEvery endpoint should include:\n ✅ Complete curl command with headers and body\n ✅ At least one success response (with realistic data)\n ✅ At least one error response (validation or auth)\n ✅ Query parameter examples for GET endpoints\n ✅ Response headers when relevant (Location, Link, etc.)\n```\n\n**Benefits:**\n- Developers copy-paste and have a working request in seconds\n- Error examples set correct expectations for client-side error handling\n- Realistic data in examples clarifies field formats and semantics\n- Reduces support tickets from confused API consumers\n\nReference: [Stripe API Documentation](https://stripe.com/docs/api) — gold standard for API examples\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5026,"content_sha256":"c42ea75242514a3a4590e7466217ebfcd51c0c8ef77a323f5c94cc9f99c8d30d"},{"filename":"rules/doc-openapi.md","content":"---\ntitle: OpenAPI/Swagger Specification\nimpact: MEDIUM\nimpactDescription: \"Cuts API integration time by 50% with machine-readable docs\"\ntags: openapi, swagger, documentation, specification\n---\n\n## OpenAPI/Swagger Specification\n\n**Impact: MEDIUM (Cuts API integration time by 50% with machine-readable docs)**\n\nAn OpenAPI specification serves as both documentation and a contract. It enables auto-generated client SDKs, interactive docs, and automated testing. Without it, developers rely on outdated wikis or reverse-engineering API behavior from code.\n\n## Incorrect\n\n```\n// ❌ No formal API documentation\n\n// Option A: Nothing at all — developers read source code\n// Option B: Outdated wiki page last edited 18 months ago\n// Option C: Slack messages like \"hey how does the /users endpoint work?\"\n// Option D: README with a few curl examples that no longer match the API\n```\n\n**Problems:**\n- Developers spend hours discovering endpoints and request formats by trial and error\n- No single source of truth — information scattered across wikis, Slack, and code comments\n- Documentation drifts out of sync with the actual API\n- Cannot auto-generate client SDKs, mock servers, or test suites\n\n## Correct\n\n### Minimal OpenAPI 3.1 Specification\n\n```yaml\n# ✅ openapi.yaml — machine-readable, always up to date\nopenapi: \"3.1.0\"\ninfo:\n title: User Management API\n version: \"1.2.0\"\n description: API for managing user accounts\n contact:\n name: API Support\n email: [email protected]\n\nservers:\n - url: https://api.example.com/v1\n description: Production\n - url: https://staging-api.example.com/v1\n description: Staging\n\nsecurity:\n - bearerAuth: []\n\npaths:\n /users:\n get:\n summary: List users\n operationId: listUsers\n tags:\n - Users\n parameters:\n - name: page\n in: query\n schema:\n type: integer\n default: 1\n - name: per_page\n in: query\n schema:\n type: integer\n default: 20\n maximum: 100\n - name: status\n in: query\n schema:\n type: string\n enum: [active, inactive, suspended]\n responses:\n \"200\":\n description: A paginated list of users\n content:\n application/json:\n schema:\n type: object\n properties:\n data:\n type: array\n items:\n $ref: \"#/components/schemas/User\"\n meta:\n $ref: \"#/components/schemas/PaginationMeta\"\n example:\n data:\n - id: 123\n first_name: Jane\n last_name: Doe\n email: [email protected]\n status: active\n created_at: \"2024-01-15T10:30:00Z\"\n meta:\n page: 1\n per_page: 20\n total: 142\n total_pages: 8\n \"401\":\n $ref: \"#/components/responses/Unauthorized\"\n\n post:\n summary: Create a user\n operationId: createUser\n tags:\n - Users\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/CreateUserRequest\"\n example:\n first_name: Jane\n last_name: Doe\n email: [email protected]\n password: secureP@ssw0rd\n responses:\n \"201\":\n description: User created successfully\n content:\n application/json:\n schema:\n type: object\n properties:\n data:\n $ref: \"#/components/schemas/User\"\n \"422\":\n $ref: \"#/components/responses/ValidationError\"\n\ncomponents:\n securitySchemes:\n bearerAuth:\n type: http\n scheme: bearer\n bearerFormat: JWT\n\n schemas:\n User:\n type: object\n properties:\n id:\n type: integer\n first_name:\n type: string\n last_name:\n type: string\n email:\n type: string\n format: email\n status:\n type: string\n enum: [active, inactive, suspended]\n created_at:\n type: string\n format: date-time\n required:\n - id\n - first_name\n - last_name\n - email\n - status\n\n CreateUserRequest:\n type: object\n properties:\n first_name:\n type: string\n minLength: 1\n maxLength: 100\n last_name:\n type: string\n minLength: 1\n maxLength: 100\n email:\n type: string\n format: email\n password:\n type: string\n minLength: 8\n required:\n - first_name\n - last_name\n - email\n - password\n\n PaginationMeta:\n type: object\n properties:\n page:\n type: integer\n per_page:\n type: integer\n total:\n type: integer\n total_pages:\n type: integer\n\n responses:\n Unauthorized:\n description: Authentication required\n content:\n application/json:\n schema:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n example: UNAUTHORIZED\n message:\n type: string\n example: Authentication required\n\n ValidationError:\n description: Request validation failed\n content:\n application/json:\n schema:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n example: VALIDATION_ERROR\n message:\n type: string\n details:\n type: array\n items:\n type: object\n properties:\n field:\n type: string\n code:\n type: string\n message:\n type: string\n```\n\n### Recommended Tools\n\n```\nSwagger UI — Interactive docs with \"Try it out\" button\nRedoc — Clean, responsive three-panel documentation\nStoplight — Visual OpenAPI editor with linting\nSpectral — OpenAPI linting and validation CLI\nopenapi-generator — Auto-generate client SDKs in 40+ languages\n```\n\n**Benefits:**\n- Single source of truth that stays in sync with the codebase\n- Auto-generate client SDKs, mock servers, and test stubs\n- Interactive documentation lets developers test endpoints in the browser\n- CI/CD can validate the spec to catch breaking changes before deploy\n\nReference: [OpenAPI Specification 3.1](https://spec.openapis.org/oas/v3.1.0)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7128,"content_sha256":"ddfcd04597e8becf71306b2bc82d2569eb3573c452776f3313dc22b3df7208a6"},{"filename":"rules/error-consistent-format.md","content":"---\ntitle: Consistent Error Response Format\nimpact: CRITICAL\nimpactDescription: \"Enables predictable error handling across API\"\ntags: errors, consistency, response-format, client-experience\n---\n\n## Consistent Error Response Format\n\n**Impact: CRITICAL (Enables predictable error handling across API)**\n\nInconsistent error formats force API consumers to handle multiple error structures, leading to fragile client code. A consistent error format makes APIs predictable, easier to debug, and simpler to integrate.\n\n## Incorrect\n\n```json\n// ❌ Different formats across endpoints\n// Endpoint A\n{ \"error\": \"Not found\" }\n\n// Endpoint B\n{ \"message\": \"Invalid email\", \"status\": 400 }\n\n// Endpoint C\n{ \"errors\": [\"Field required\", \"Invalid format\"] }\n\n// Endpoint D\n{\n \"success\": false,\n \"errorMessage\": \"Something went wrong\"\n}\n\n// Endpoint E - just a string\n\"User not found\"\n```\n\n**Problems:**\n- Clients can't predict error structure\n- Different parsing logic needed for each endpoint\n- Hard to build generic error handlers\n- Inconsistent developer experience\n\n## Correct\n\n### Standard Error Envelope\n\n```json\n// ✅ Every error follows the same structure\n{\n \"error\": {\n \"code\": \"ERROR_CODE\",\n \"message\": \"Human-readable message\",\n \"details\": [],\n \"request_id\": \"req_abc123\"\n }\n}\n```\n\n### Validation Errors (422)\n\n```json\n{\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"The request contains invalid data\",\n \"details\": [\n {\n \"field\": \"email\",\n \"code\": \"INVALID_FORMAT\",\n \"message\": \"Please provide a valid email address\"\n },\n {\n \"field\": \"password\",\n \"code\": \"TOO_SHORT\",\n \"message\": \"Password must be at least 8 characters\",\n \"meta\": {\n \"min_length\": 8,\n \"actual_length\": 5\n }\n },\n {\n \"field\": \"age\",\n \"code\": \"OUT_OF_RANGE\",\n \"message\": \"Age must be between 18 and 120\",\n \"meta\": {\n \"min\": 18,\n \"max\": 120,\n \"actual\": 15\n }\n }\n ],\n \"request_id\": \"req_abc123\"\n }\n}\n```\n\n### Not Found (404)\n\n```json\n{\n \"error\": {\n \"code\": \"NOT_FOUND\",\n \"message\": \"User with ID 'usr_123' not found\",\n \"details\": [\n {\n \"resource\": \"user\",\n \"field\": \"id\",\n \"value\": \"usr_123\"\n }\n ],\n \"request_id\": \"req_def456\"\n }\n}\n```\n\n### Authentication Error (401)\n\n```json\n{\n \"error\": {\n \"code\": \"UNAUTHORIZED\",\n \"message\": \"Authentication required\",\n \"details\": [\n {\n \"code\": \"TOKEN_EXPIRED\",\n \"message\": \"Your access token has expired\"\n }\n ],\n \"request_id\": \"req_ghi789\"\n }\n}\n```\n\n### Authorization Error (403)\n\n```json\n{\n \"error\": {\n \"code\": \"FORBIDDEN\",\n \"message\": \"You don't have permission to access this resource\",\n \"details\": [\n {\n \"resource\": \"order\",\n \"action\": \"delete\",\n \"reason\": \"Only admins can delete orders\"\n }\n ],\n \"request_id\": \"req_jkl012\"\n }\n}\n```\n\n### Conflict Error (409)\n\n```json\n{\n \"error\": {\n \"code\": \"CONFLICT\",\n \"message\": \"A user with this email already exists\",\n \"details\": [\n {\n \"field\": \"email\",\n \"code\": \"DUPLICATE\",\n \"value\": \"[email protected]\"\n }\n ],\n \"request_id\": \"req_mno345\"\n }\n}\n```\n\n### Rate Limit Error (429)\n\n```json\n{\n \"error\": {\n \"code\": \"RATE_LIMITED\",\n \"message\": \"Too many requests. Please retry after 60 seconds.\",\n \"details\": [\n {\n \"limit\": 100,\n \"window\": \"1 minute\",\n \"retry_after\": 60\n }\n ],\n \"request_id\": \"req_pqr678\"\n }\n}\n```\n\n### Server Error (500)\n\n```json\n{\n \"error\": {\n \"code\": \"INTERNAL_ERROR\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"request_id\": \"req_stu901\"\n }\n}\n```\n\n**Note:** Never expose stack traces or internal details in production.\n\n## Implementation\n\n### TypeScript/Node.js\n\n```typescript\n// ✅ Error classes\nabstract class AppError extends Error {\n abstract readonly code: string;\n abstract readonly statusCode: number;\n readonly details: ErrorDetail[];\n\n constructor(message: string, details: ErrorDetail[] = []) {\n super(message);\n this.details = details;\n }\n\n toJSON() {\n return {\n error: {\n code: this.code,\n message: this.message,\n details: this.details.length > 0 ? this.details : undefined,\n }\n };\n }\n}\n\nclass ValidationError extends AppError {\n readonly code = 'VALIDATION_ERROR';\n readonly statusCode = 422;\n}\n\nclass NotFoundError extends AppError {\n readonly code = 'NOT_FOUND';\n readonly statusCode = 404;\n}\n\nclass UnauthorizedError extends AppError {\n readonly code = 'UNAUTHORIZED';\n readonly statusCode = 401;\n}\n\n// Error handler middleware\nfunction errorHandler(err, req, res, next) {\n const requestId = req.id || generateRequestId();\n\n if (err instanceof AppError) {\n return res.status(err.statusCode).json({\n error: {\n ...err.toJSON().error,\n request_id: requestId,\n }\n });\n }\n\n // Log unexpected errors\n logger.error('Unexpected error', { error: err, requestId });\n\n // Generic response for unknown errors\n res.status(500).json({\n error: {\n code: 'INTERNAL_ERROR',\n message: 'An unexpected error occurred',\n request_id: requestId,\n }\n });\n}\n```\n\n### Laravel/PHP\n\n```php\n\u003c?php\n\n// ✅ app/Exceptions/AppException.php\nabstract class AppException extends Exception\n{\n abstract public function getErrorCode(): string;\n abstract public function getStatusCode(): int;\n\n protected array $details = [];\n\n public function setDetails(array $details): self\n {\n $this->details = $details;\n return $this;\n }\n\n public function render(): JsonResponse\n {\n return response()->json([\n 'error' => [\n 'code' => $this->getErrorCode(),\n 'message' => $this->getMessage(),\n 'details' => $this->details ?: null,\n 'request_id' => request()->id(),\n ],\n ], $this->getStatusCode());\n }\n}\n\nclass ValidationException extends AppException\n{\n public function getErrorCode(): string\n {\n return 'VALIDATION_ERROR';\n }\n\n public function getStatusCode(): int\n {\n return 422;\n }\n\n public static function fromValidator(Validator $validator): self\n {\n $details = [];\n foreach ($validator->errors()->toArray() as $field => $messages) {\n $details[] = [\n 'field' => $field,\n 'code' => 'INVALID',\n 'message' => $messages[0],\n ];\n }\n\n return (new self('The request contains invalid data'))\n ->setDetails($details);\n }\n}\n\n// Handler\nclass Handler extends ExceptionHandler\n{\n public function render($request, Throwable $e)\n {\n if ($e instanceof AppException) {\n return $e->render();\n }\n\n if ($e instanceof ModelNotFoundException) {\n return response()->json([\n 'error' => [\n 'code' => 'NOT_FOUND',\n 'message' => 'Resource not found',\n 'request_id' => $request->id(),\n ],\n ], 404);\n }\n\n // Log and return generic error\n Log::error($e->getMessage(), ['exception' => $e]);\n\n return response()->json([\n 'error' => [\n 'code' => 'INTERNAL_ERROR',\n 'message' => 'An unexpected error occurred',\n 'request_id' => $request->id(),\n ],\n ], 500);\n }\n}\n```\n\n## Error Code Naming\n\n```\n// ✅ Use SCREAMING_SNAKE_CASE\nVALIDATION_ERROR\nNOT_FOUND\nUNAUTHORIZED\nFORBIDDEN\nRATE_LIMITED\nINTERNAL_ERROR\n\n// ✅ Be specific\nINVALID_EMAIL_FORMAT\nPASSWORD_TOO_SHORT\nDUPLICATE_EMAIL\nTOKEN_EXPIRED\nINSUFFICIENT_FUNDS\n\n// ❌ Avoid vague codes\nERROR\nFAILED\nBAD_REQUEST\n```\n\n**Benefits:**\n- Predictable API behavior across all endpoints\n- Reusable client-side error handling logic\n- Easier debugging with request IDs\n- Clear error codes for programmatic handling\n- Human-readable messages for display\n- Detailed validation feedback for field-level errors\n\nReference: [RFC 7807 - Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8260,"content_sha256":"fbc336b5db5a30659f02e46fdf5cf90840ab8d7117547076fd7f29568413e24d"},{"filename":"rules/error-error-codes.md","content":"---\ntitle: Use Machine-Readable Error Codes\nimpact: CRITICAL\nimpactDescription: \"Enables programmatic error handling and client recovery\"\ntags: errors, error-codes, automation, monitoring\n---\n\n## Use Machine-Readable Error Codes\n\n**Impact: CRITICAL (Enables programmatic error handling and client recovery)**\n\nInclude standardized, machine-readable error codes alongside human-readable messages to enable programmatic error handling.\n\n## Incorrect\n\n```json\n// ❌ Only human-readable messages\n{\n \"error\": \"The user was not found\"\n}\n\n// ❌ HTTP status codes only\n{\n \"status\": 404\n}\n\n// ❌ Inconsistent or vague codes\n{\n \"error_code\": \"ERR001\"\n}\n\n{\n \"code\": 1234\n}\n\n{\n \"error_type\": \"bad_thing_happened\"\n}\n```\n\n```javascript\n// ❌ No error codes for programmatic handling\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n if (!user) {\n // Client can't reliably detect \"not found\" vs other 404s\n res.status(404).json({ message: 'User not found' });\n }\n});\n```\n\n**Problems:**\n- Clients must parse human-readable messages which change with localization\n- No stable identifier for programmatic error handling\n- Numeric codes are meaningless without a reference table\n- Inconsistent code formats across endpoints\n- Cannot implement targeted recovery strategies per error type\n- Monitoring dashboards cannot categorize errors precisely\n\n## Correct\n\n```javascript\n// ✅ Define error codes as constants\nconst ErrorCodes = {\n // Authentication & Authorization\n AUTH_TOKEN_MISSING: 'auth_token_missing',\n AUTH_TOKEN_INVALID: 'auth_token_invalid',\n AUTH_TOKEN_EXPIRED: 'auth_token_expired',\n AUTH_INSUFFICIENT_PERMISSIONS: 'auth_insufficient_permissions',\n\n // Validation\n VALIDATION_ERROR: 'validation_error',\n VALIDATION_REQUIRED_FIELD: 'validation_required_field',\n VALIDATION_INVALID_FORMAT: 'validation_invalid_format',\n VALIDATION_OUT_OF_RANGE: 'validation_out_of_range',\n\n // Resources\n RESOURCE_NOT_FOUND: 'resource_not_found',\n RESOURCE_ALREADY_EXISTS: 'resource_already_exists',\n RESOURCE_CONFLICT: 'resource_conflict',\n RESOURCE_DELETED: 'resource_deleted',\n\n // Rate Limiting\n RATE_LIMIT_EXCEEDED: 'rate_limit_exceeded',\n\n // Business Logic\n INSUFFICIENT_FUNDS: 'insufficient_funds',\n INVENTORY_UNAVAILABLE: 'inventory_unavailable',\n ORDER_CANNOT_BE_CANCELLED: 'order_cannot_be_cancelled',\n SUBSCRIPTION_EXPIRED: 'subscription_expired',\n\n // Server Errors\n INTERNAL_ERROR: 'internal_error',\n SERVICE_UNAVAILABLE: 'service_unavailable',\n DEPENDENCY_ERROR: 'dependency_error'\n};\n\n// Error factory\nclass APIError extends Error {\n constructor(code, message, statusCode = 400, details = null) {\n super(message);\n this.code = code;\n this.statusCode = statusCode;\n this.details = details;\n }\n}\n\n// Usage in routes\napp.get('/users/:id', async (req, res, next) => {\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n throw new APIError(\n ErrorCodes.RESOURCE_NOT_FOUND,\n 'User not found',\n 404,\n { resourceType: 'user', resourceId: req.params.id }\n );\n }\n res.json(user);\n } catch (error) {\n next(error);\n }\n});\n\napp.post('/orders', async (req, res, next) => {\n try {\n const product = await db.findProduct(req.body.productId);\n\n if (product.stock \u003c req.body.quantity) {\n throw new APIError(\n ErrorCodes.INVENTORY_UNAVAILABLE,\n 'Not enough items in stock',\n 422,\n {\n requested: req.body.quantity,\n available: product.stock,\n productId: req.body.productId\n }\n );\n }\n\n // Process order...\n } catch (error) {\n next(error);\n }\n});\n\n// Error handler\napp.use((err, req, res, next) => {\n if (err instanceof APIError) {\n return res.status(err.statusCode).json({\n error: {\n code: err.code,\n message: err.message,\n details: err.details\n }\n });\n }\n\n // Unknown error\n res.status(500).json({\n error: {\n code: ErrorCodes.INTERNAL_ERROR,\n message: 'An unexpected error occurred'\n }\n });\n});\n```\n\n```python\n# ✅ Python with error codes\nfrom enum import Enum\nfrom fastapi import FastAPI, HTTPException\nfrom typing import Optional, Any\n\nclass ErrorCode(str, Enum):\n # Authentication\n AUTH_TOKEN_MISSING = \"auth_token_missing\"\n AUTH_TOKEN_INVALID = \"auth_token_invalid\"\n AUTH_TOKEN_EXPIRED = \"auth_token_expired\"\n AUTH_INSUFFICIENT_PERMISSIONS = \"auth_insufficient_permissions\"\n\n # Validation\n VALIDATION_ERROR = \"validation_error\"\n VALIDATION_REQUIRED_FIELD = \"validation_required_field\"\n\n # Resources\n RESOURCE_NOT_FOUND = \"resource_not_found\"\n RESOURCE_ALREADY_EXISTS = \"resource_already_exists\"\n RESOURCE_CONFLICT = \"resource_conflict\"\n\n # Business Logic\n INSUFFICIENT_FUNDS = \"insufficient_funds\"\n INVENTORY_UNAVAILABLE = \"inventory_unavailable\"\n\n # Server\n INTERNAL_ERROR = \"internal_error\"\n SERVICE_UNAVAILABLE = \"service_unavailable\"\n\nclass APIError(Exception):\n def __init__(\n self,\n code: ErrorCode,\n message: str,\n status_code: int = 400,\n details: Optional[dict] = None\n ):\n self.code = code\n self.message = message\n self.status_code = status_code\n self.details = details\n\napp = FastAPI()\n\[email protected]_handler(APIError)\nasync def api_error_handler(request, exc: APIError):\n return JSONResponse(\n status_code=exc.status_code,\n content={\n \"error\": {\n \"code\": exc.code.value,\n \"message\": exc.message,\n \"details\": exc.details\n }\n }\n )\n\[email protected](\"/users/{user_id}\")\nasync def get_user(user_id: int):\n user = await db.get_user(user_id)\n if not user:\n raise APIError(\n code=ErrorCode.RESOURCE_NOT_FOUND,\n message=f\"User with ID {user_id} not found\",\n status_code=404,\n details={\"resource_type\": \"user\", \"resource_id\": user_id}\n )\n return user\n```\n\n```json\n// ✅ Error response with code\n{\n \"error\": {\n \"code\": \"inventory_unavailable\",\n \"message\": \"Not enough items in stock to fulfill your order\",\n \"details\": {\n \"productId\": \"prod_123\",\n \"productName\": \"Widget Pro\",\n \"requested\": 10,\n \"available\": 3\n }\n }\n}\n```\n\n```typescript\n// ✅ Client-side error handling\nasync function createOrder(orderData: OrderData): Promise\u003cOrder> {\n const response = await fetch('/api/orders', {\n method: 'POST',\n body: JSON.stringify(orderData)\n });\n\n if (!response.ok) {\n const error = await response.json();\n\n // Programmatic handling based on error code\n switch (error.error.code) {\n case 'inventory_unavailable':\n showInventoryWarning(error.error.details);\n break;\n\n case 'insufficient_funds':\n redirectToPaymentUpdate();\n break;\n\n case 'auth_token_expired':\n await refreshToken();\n return createOrder(orderData); // Retry\n\n case 'rate_limit_exceeded':\n await delay(error.error.details.retryAfter * 1000);\n return createOrder(orderData); // Retry\n\n default:\n showGenericError(error.error.message);\n }\n\n throw new APIError(error);\n }\n\n return response.json();\n}\n```\n\n## Error Code Naming Conventions\n\n| Category | Pattern | Examples |\n|----------|---------|----------|\n| Auth | `auth_*` | `auth_token_expired`, `auth_invalid_credentials` |\n| Validation | `validation_*` | `validation_error`, `validation_invalid_email` |\n| Resource | `resource_*` | `resource_not_found`, `resource_conflict` |\n| Business | Domain-specific | `insufficient_funds`, `inventory_unavailable` |\n| Rate Limit | `rate_limit_*` | `rate_limit_exceeded` |\n| Server | `internal_*` or `service_*` | `internal_error`, `service_unavailable` |\n\n**Benefits:**\n- Code can switch on error codes to take appropriate recovery action\n- Error codes remain stable even when messages change or are localized\n- Error codes can be documented and referenced in API docs\n- Precise alerting and monitoring dashboards based on error categories\n- Tests can assert on specific error codes rather than message strings\n- Messages can be translated while codes stay constant across locales\n\nReference: [Google Cloud API Error Model](https://cloud.google.com/apis/design/errors)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8346,"content_sha256":"b333811b2da665cf774440afaf3f7c9d42e729ee6441fdf37161bbf3767d1974"},{"filename":"rules/error-meaningful-messages.md","content":"---\ntitle: Provide Meaningful Error Messages\nimpact: CRITICAL\nimpactDescription: \"Reduces support burden and improves developer experience\"\ntags: errors, messages, user-experience, actionable\n---\n\n## Provide Meaningful Error Messages\n\n**Impact: CRITICAL (Reduces support burden and improves developer experience)**\n\nError messages should be clear, actionable, and help users understand what went wrong and how to fix it.\n\n## Incorrect\n\n```json\n// ❌ Vague or unhelpful messages\n{\n \"error\": \"Error\"\n}\n\n{\n \"error\": \"Bad request\"\n}\n\n{\n \"error\": \"Invalid input\"\n}\n\n{\n \"error\": \"Something went wrong\"\n}\n\n{\n \"error\": \"null\"\n}\n\n{\n \"error\": \"Error code: 0x8004005\"\n}\n\n// ❌ Technical jargon users can't understand\n{\n \"error\": \"SQLITE_CONSTRAINT_FOREIGNKEY\"\n}\n\n{\n \"error\": \"MongoServerError: E11000 duplicate key error\"\n}\n```\n\n```javascript\n// ❌ Unhelpful error responses\napp.post('/users', async (req, res) => {\n try {\n await db.createUser(req.body);\n } catch (error) {\n res.status(400).json({ error: 'Bad request' }); // What's bad about it?\n }\n});\n```\n\n**Problems:**\n- Users cannot determine what went wrong or how to fix it\n- Developers waste time debugging vague error messages\n- Support burden increases with every unclear error\n- Internal database errors leak implementation details\n- No actionable guidance for the next step\n\n## Correct\n\n```javascript\n// ✅ Clear, actionable error messages\nconst errorMessages = {\n email_required: 'Email address is required to create an account',\n email_invalid: 'Please provide a valid email address (e.g., [email protected])',\n email_taken: 'An account with this email already exists. Try signing in instead',\n password_weak: 'Password must be at least 8 characters with one uppercase, one lowercase, and one number',\n rate_limited: 'Too many requests. Please wait 60 seconds before trying again',\n resource_not_found: 'The requested user could not be found. It may have been deleted',\n permission_denied: 'You do not have permission to access this resource. Contact your administrator',\n payment_failed: 'Your payment could not be processed. Please check your card details and try again'\n};\n\napp.post('/users', async (req, res, next) => {\n try {\n const { email, password, name } = req.body;\n\n // Specific validation messages\n if (!email) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Email address is required to create an account',\n field: 'email'\n }\n });\n }\n\n if (!isValidEmail(email)) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Please provide a valid email address (e.g., [email protected])',\n field: 'email',\n provided: email\n }\n });\n }\n\n const existingUser = await db.findUserByEmail(email);\n if (existingUser) {\n return res.status(409).json({\n error: {\n code: 'resource_conflict',\n message: 'An account with this email already exists. Try signing in or resetting your password',\n field: 'email',\n links: {\n signin: '/auth/signin',\n passwordReset: '/auth/password-reset'\n }\n }\n });\n }\n\n const user = await db.createUser({ email, password, name });\n res.status(201).json(user);\n\n } catch (error) {\n next(error);\n }\n});\n```\n\n```python\n# ✅ FastAPI with meaningful errors\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel, validator, EmailStr\n\napp = FastAPI()\n\nclass UserCreate(BaseModel):\n email: EmailStr\n password: str\n name: str\n\n @validator('password')\n def password_strength(cls, v):\n if len(v) \u003c 8:\n raise ValueError(\n 'Password must be at least 8 characters long. '\n 'Strong passwords help protect your account.'\n )\n if not any(c.isupper() for c in v):\n raise ValueError(\n 'Password must contain at least one uppercase letter (A-Z)'\n )\n if not any(c.islower() for c in v):\n raise ValueError(\n 'Password must contain at least one lowercase letter (a-z)'\n )\n if not any(c.isdigit() for c in v):\n raise ValueError(\n 'Password must contain at least one number (0-9)'\n )\n return v\n\n @validator('name')\n def name_not_empty(cls, v):\n if not v or not v.strip():\n raise ValueError(\n 'Name is required. Please enter your full name as you would '\n 'like it to appear on your profile.'\n )\n if len(v) > 100:\n raise ValueError(\n f'Name must be 100 characters or less. You entered {len(v)} characters.'\n )\n return v.strip()\n\[email protected](\"/users\")\nasync def create_user(user: UserCreate):\n existing = await db.find_user_by_email(user.email)\n if existing:\n raise HTTPException(\n status_code=409,\n detail={\n \"code\": \"email_already_registered\",\n \"message\": (\n f\"The email '{user.email}' is already registered. \"\n \"If this is your email, try signing in or resetting your password.\"\n ),\n \"suggestions\": [\n \"Sign in with your existing account\",\n \"Reset your password if you forgot it\",\n \"Use a different email address\"\n ]\n }\n )\n return await db.create_user(user)\n```\n\n```json\n// ✅ Good error response examples\n\n// Validation error with guidance\n{\n \"error\": {\n \"code\": \"validation_error\",\n \"message\": \"The email address format is invalid\",\n \"details\": [\n {\n \"field\": \"email\",\n \"message\": \"Please provide a valid email address (e.g., [email protected])\",\n \"provided\": \"not-an-email\",\n \"suggestion\": \"Check for typos and ensure the email includes an @ symbol\"\n }\n ]\n }\n}\n\n// Resource not found with context\n{\n \"error\": {\n \"code\": \"resource_not_found\",\n \"message\": \"Order #12345 could not be found\",\n \"details\": [\n {\n \"message\": \"This order may have been deleted or the ID may be incorrect\",\n \"suggestions\": [\n \"Verify the order ID is correct\",\n \"Check your order history for the correct ID\",\n \"The order may have been archived after 90 days\"\n ]\n }\n ]\n }\n}\n\n// Permission error with next steps\n{\n \"error\": {\n \"code\": \"permission_denied\",\n \"message\": \"You don't have permission to delete this project\",\n \"details\": [\n {\n \"message\": \"Only project owners and administrators can delete projects\",\n \"currentRole\": \"member\",\n \"requiredRoles\": [\"owner\", \"admin\"],\n \"suggestion\": \"Contact the project owner to request deletion or elevated permissions\"\n }\n ]\n }\n}\n\n// Rate limit with retry info\n{\n \"error\": {\n \"code\": \"rate_limit_exceeded\",\n \"message\": \"You've made too many requests. Please slow down.\",\n \"details\": [\n {\n \"limit\": 100,\n \"window\": \"1 minute\",\n \"retryAfter\": 45,\n \"message\": \"You can make another request in 45 seconds\"\n }\n ]\n }\n}\n```\n\n**Benefits:**\n- Clear messages help users fix problems without contacting support\n- Self-explanatory errors reduce support ticket volume\n- API consumers can debug issues faster with meaningful context\n- Actionable guidance tells users what to do next, not just what went wrong\n- Professional error messages build confidence in your API\n- Clear messages are easier to translate for localization\n\nReference: [Microsoft REST API Guidelines - Errors](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7861,"content_sha256":"b624c3ac059d45fc9eff4c03b5d9328ed5ffc8e2c2ea05ce8d954e06b2023e4a"},{"filename":"rules/error-no-stack-traces.md","content":"---\ntitle: Never Expose Stack Traces in Production\nimpact: CRITICAL\nimpactDescription: \"Prevents security vulnerabilities and information disclosure\"\ntags: errors, security, production, sensitive-data\n---\n\n## Never Expose Stack Traces in Production\n\n**Impact: CRITICAL (Prevents security vulnerabilities and information disclosure)**\n\nStack traces and internal error details should never be exposed to API clients in production environments, as they reveal implementation details and potential vulnerabilities.\n\n## Incorrect\n\n```json\n// ❌ Full stack trace in production response\n{\n \"error\": \"Cannot read property 'id' of undefined\",\n \"stack\": \"TypeError: Cannot read property 'id' of undefined\\n at getUserOrders (/app/src/controllers/orders.js:45:23)\\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\\n at next (/app/node_modules/express/lib/router/route.js:137:13)\\n at authenticate (/app/src/middleware/auth.js:28:5)\\n at /app/node_modules/express/lib/router/index.js:284:15\"\n}\n\n// ❌ Database error details exposed\n{\n \"error\": \"SequelizeConnectionError: Connection refused to host 'db.internal.company.com' port 5432\",\n \"sql\": \"SELECT * FROM users WHERE id = 1 AND deleted_at IS NULL\"\n}\n\n// ❌ Internal paths and configuration\n{\n \"error\": \"ENOENT: no such file or directory, open '/var/app/config/secrets.json'\"\n}\n```\n\n```javascript\n// ❌ Exposing all error details\napp.use((err, req, res, next) => {\n res.status(500).json({\n error: err.message,\n stack: err.stack, // Never do this in production!\n code: err.code\n });\n});\n```\n\n**Problems:**\n- Stack traces reveal file paths, dependencies, and code structure attackers can exploit\n- Database error messages may expose schemas, connection strings, or query logic\n- Internal file paths reveal server configuration and directory structure\n- Framework and version information helps attackers find known vulnerabilities\n- Violates security standards like PCI-DSS and SOC 2\n\n## Correct\n\n```javascript\n// ✅ Secure error handler\nconst isProduction = process.env.NODE_ENV === 'production';\n\napp.use((err, req, res, next) => {\n // Log full error internally\n logger.error('Request error', {\n error: err.message,\n stack: err.stack,\n requestId: req.id,\n path: req.path,\n method: req.method,\n userId: req.user?.id\n });\n\n // Determine if error is safe to expose\n const isOperationalError = err.isOperational || err.expose;\n const statusCode = err.statusCode || 500;\n\n // Build safe response\n const errorResponse = {\n error: {\n code: err.code || 'internal_error',\n message: isOperationalError\n ? err.message\n : 'An unexpected error occurred. Please try again later.',\n requestId: req.id\n }\n };\n\n // Only include details in development\n if (!isProduction && err.stack) {\n errorResponse.error._debug = {\n message: err.message,\n stack: err.stack.split('\\n')\n };\n }\n\n res.status(statusCode).json(errorResponse);\n});\n\n// Custom error class for operational errors\nclass APIError extends Error {\n constructor(message, statusCode = 500, code = 'internal_error') {\n super(message);\n this.statusCode = statusCode;\n this.code = code;\n this.isOperational = true; // Safe to expose\n }\n}\n\n// Database error handling\napp.get('/users/:id', async (req, res, next) => {\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n throw new APIError('User not found', 404, 'resource_not_found');\n }\n res.json(user);\n } catch (error) {\n if (error instanceof APIError) {\n return next(error);\n }\n\n // Log the actual database error\n logger.error('Database error', {\n error: error.message,\n stack: error.stack,\n query: 'findUser',\n params: { id: req.params.id }\n });\n\n // Return generic error to client\n next(new APIError(\n 'Unable to retrieve user information',\n 500,\n 'service_error'\n ));\n }\n});\n```\n\n```python\n# ✅ FastAPI with secure error handling\nimport logging\nimport traceback\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\nimport os\n\napp = FastAPI()\nlogger = logging.getLogger(__name__)\nIS_PRODUCTION = os.getenv(\"ENVIRONMENT\") == \"production\"\n\nclass APIError(Exception):\n def __init__(self, message: str, status_code: int = 500, code: str = \"internal_error\"):\n self.message = message\n self.status_code = status_code\n self.code = code\n self.is_operational = True\n\[email protected]_handler(APIError)\nasync def api_error_handler(request: Request, exc: APIError):\n return JSONResponse(\n status_code=exc.status_code,\n content={\n \"error\": {\n \"code\": exc.code,\n \"message\": exc.message,\n \"request_id\": request.state.request_id\n }\n }\n )\n\[email protected]_handler(Exception)\nasync def generic_error_handler(request: Request, exc: Exception):\n # Log full error internally\n logger.error(\n \"Unhandled exception\",\n extra={\n \"error\": str(exc),\n \"traceback\": traceback.format_exc(),\n \"request_id\": request.state.request_id,\n \"path\": request.url.path,\n \"method\": request.method\n }\n )\n\n # Return safe response\n content = {\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"request_id\": request.state.request_id\n }\n }\n\n # Include debug info only in development\n if not IS_PRODUCTION:\n content[\"error\"][\"_debug\"] = {\n \"type\": type(exc).__name__,\n \"message\": str(exc),\n \"traceback\": traceback.format_exc().split(\"\\n\")\n }\n\n return JSONResponse(status_code=500, content=content)\n```\n\n```json\n// ✅ Production error response (safe)\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"requestId\": \"req-abc123\"\n }\n}\n\n// ✅ Development error response (with debug info)\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again later.\",\n \"requestId\": \"req-abc123\",\n \"_debug\": {\n \"type\": \"TypeError\",\n \"message\": \"Cannot read property 'id' of undefined\",\n \"traceback\": [\n \"Traceback (most recent call last):\",\n \" File \\\"app.py\\\", line 45, in get_user\",\n \" return user.id\",\n \"TypeError: Cannot read property 'id' of undefined\"\n ]\n }\n }\n}\n```\n\n## What to Log vs. What to Return\n\n| Information | Log Internally | Return to Client |\n|-------------|----------------|------------------|\n| Error message | Yes | Generic only |\n| Stack trace | Yes | Never in production |\n| SQL queries | Yes | Never |\n| File paths | Yes | Never |\n| Internal IPs | Yes | Never |\n| Request ID | Yes | Yes |\n| Error code | Yes | Yes |\n| User ID | Yes | No |\n| Timestamps | Yes | Optional |\n\n**Benefits:**\n- Prevents attackers from exploiting revealed file paths, dependencies, and code structure\n- Protects database schemas, API keys, and other secrets from leaking\n- Hides framework versions that could expose known vulnerabilities\n- Clean error messages present a professional API to consumers\n- Meets compliance requirements for PCI-DSS, SOC 2, and similar standards\n- Request IDs enable correlation between client reports and internal logs\n\nReference: [OWASP Improper Error Handling](https://owasp.org/www-community/Improper_Error_Handling)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7579,"content_sha256":"393c4eacd33a7fa4001101e65d202d6f9d7b13af75947ad791e118adbc0709ba"},{"filename":"rules/error-request-id.md","content":"---\ntitle: Include Request ID in Error Responses\nimpact: CRITICAL\nimpactDescription: \"Enables log correlation and efficient debugging\"\ntags: errors, debugging, logging, request-tracking\n---\n\n## Include Request ID in Error Responses\n\n**Impact: CRITICAL (Enables log correlation and efficient debugging)**\n\nEvery API request should have a unique identifier that appears in both the response and server logs, enabling easy correlation for debugging.\n\n## Incorrect\n\n```json\n// ❌ No request identifier\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred\"\n }\n}\n// User reports error, but support can't find it in logs\n```\n\n```javascript\n// ❌ No request tracking\napp.get('/users/:id', async (req, res) => {\n try {\n const user = await db.findUser(req.params.id);\n res.json(user);\n } catch (error) {\n console.log('Error:', error.message); // No way to correlate\n res.status(500).json({ error: 'Something went wrong' });\n }\n});\n```\n\n**Problems:**\n- Support cannot locate specific errors in logs when users report issues\n- No way to correlate client-side errors with server-side logs\n- Debugging requires time-based log searching which is imprecise\n- Distributed systems cannot trace requests across services\n- No audit trail for specific request flows\n\n## Correct\n\n```javascript\n// ✅ Request ID middleware\nconst { v4: uuidv4 } = require('uuid');\n\napp.use((req, res, next) => {\n // Use client-provided ID or generate new one\n req.id = req.headers['x-request-id'] || uuidv4();\n\n // Add to response headers\n res.setHeader('X-Request-ID', req.id);\n\n // Add to logger context\n req.log = logger.child({ requestId: req.id });\n\n next();\n});\n\n// Use in routes\napp.get('/users/:id', async (req, res, next) => {\n req.log.info('Fetching user', { userId: req.params.id });\n\n try {\n const user = await db.findUser(req.params.id);\n if (!user) {\n return res.status(404).json({\n error: {\n code: 'resource_not_found',\n message: 'User not found',\n requestId: req.id\n }\n });\n }\n res.json(user);\n } catch (error) {\n req.log.error('Failed to fetch user', {\n error: error.message,\n stack: error.stack\n });\n next(error);\n }\n});\n\n// Error handler includes request ID\napp.use((err, req, res, next) => {\n req.log.error('Request failed', {\n error: err.message,\n stack: err.stack,\n statusCode: err.statusCode || 500\n });\n\n res.status(err.statusCode || 500).json({\n error: {\n code: err.code || 'internal_error',\n message: err.message || 'An unexpected error occurred',\n requestId: req.id\n }\n });\n});\n```\n\n```python\n# ✅ FastAPI with request ID\nimport uuid\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\nimport logging\nfrom contextvars import ContextVar\n\napp = FastAPI()\nlogger = logging.getLogger(__name__)\n\n# Context variable for request ID\nrequest_id_var: ContextVar[str] = ContextVar(\"request_id\", default=\"\")\n\[email protected](\"http\")\nasync def request_id_middleware(request: Request, call_next):\n # Get or generate request ID\n request_id = request.headers.get(\"X-Request-ID\", str(uuid.uuid4()))\n request.state.request_id = request_id\n request_id_var.set(request_id)\n\n # Process request\n response = await call_next(request)\n\n # Add request ID to response\n response.headers[\"X-Request-ID\"] = request_id\n return response\n\n# Custom log filter to include request ID\nclass RequestIdFilter(logging.Filter):\n def filter(self, record):\n record.request_id = request_id_var.get(\"\")\n return True\n\n# Configure logging\nhandler = logging.StreamHandler()\nhandler.addFilter(RequestIdFilter())\nhandler.setFormatter(logging.Formatter(\n '%(asctime)s [%(request_id)s] %(levelname)s: %(message)s'\n))\nlogger.addHandler(handler)\n\[email protected]_handler(Exception)\nasync def error_handler(request: Request, exc: Exception):\n logger.error(f\"Request failed: {exc}\", exc_info=True)\n\n return JSONResponse(\n status_code=500,\n content={\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred\",\n \"requestId\": request.state.request_id\n }\n },\n headers={\"X-Request-ID\": request.state.request_id}\n )\n\[email protected](\"/users/{user_id}\")\nasync def get_user(user_id: int, request: Request):\n logger.info(f\"Fetching user {user_id}\")\n\n user = await db.get_user(user_id)\n if not user:\n return JSONResponse(\n status_code=404,\n content={\n \"error\": {\n \"code\": \"resource_not_found\",\n \"message\": f\"User {user_id} not found\",\n \"requestId\": request.state.request_id\n }\n }\n )\n return user\n```\n\n```json\n// ✅ Error response with request ID\nHTTP/1.1 500 Internal Server Error\nX-Request-ID: req-550e8400-e29b-41d4-a716-446655440000\n\n{\n \"error\": {\n \"code\": \"internal_error\",\n \"message\": \"An unexpected error occurred. Please try again.\",\n \"requestId\": \"req-550e8400-e29b-41d4-a716-446655440000\"\n }\n}\n```\n\n```bash\n# ✅ Server logs with request ID\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] INFO: Fetching user 123\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] ERROR: Database connection timeout\n2024-01-15 10:30:00 [req-550e8400-e29b-41d4-a716-446655440000] ERROR: Request failed\n```\n\n## Distributed Tracing Integration\n\n```javascript\n// ✅ Integration with OpenTelemetry\nconst { trace, context } = require('@opentelemetry/api');\n\napp.use((req, res, next) => {\n const span = trace.getActiveSpan();\n\n // Use trace ID as request ID for distributed tracing\n if (span) {\n const traceId = span.spanContext().traceId;\n req.id = traceId;\n req.spanContext = span.spanContext();\n } else {\n req.id = uuidv4();\n }\n\n res.setHeader('X-Request-ID', req.id);\n next();\n});\n```\n\n```yaml\n# ✅ OpenAPI documentation for request ID\ncomponents:\n headers:\n X-Request-ID:\n description: Unique identifier for the request, used for debugging and log correlation\n schema:\n type: string\n format: uuid\n example: \"550e8400-e29b-41d4-a716-446655440000\"\n\n schemas:\n Error:\n type: object\n properties:\n error:\n type: object\n properties:\n code:\n type: string\n message:\n type: string\n requestId:\n type: string\n description: Unique request identifier for support correlation\n```\n\n**Benefits:**\n- Users can provide the request ID when reporting issues for instant log lookup\n- Links all log entries for a single request across multiple services\n- Request IDs propagate through microservices for end-to-end distributed tracing\n- \"Please provide the request ID\" is faster than \"describe what you did\"\n- Enables tracking individual request paths through infrastructure\n- Audit trails require the ability to trace specific requests\n\nReference: [OpenTelemetry Tracing](https://opentelemetry.io/docs/concepts/signals/traces/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7158,"content_sha256":"e21b6dc54ee34877b317e279f667dd6340b4ac5745e797ac318dcada5582e417"},{"filename":"rules/error-validation-details.md","content":"---\ntitle: Include Validation Error Details\nimpact: CRITICAL\nimpactDescription: \"Enables field-level error feedback for better UX\"\ntags: errors, validation, form-handling, user-experience\n---\n\n## Include Validation Error Details\n\n**Impact: CRITICAL (Enables field-level error feedback for better UX)**\n\nWhen validation fails, provide specific details about which fields failed and why, enabling clients to display targeted error messages.\n\n## Incorrect\n\n```json\n// ❌ Single vague validation error\n{\n \"error\": \"Validation failed\"\n}\n\n// ❌ List without field association\n{\n \"errors\": [\n \"Invalid email\",\n \"Password too short\",\n \"Name required\"\n ]\n}\n\n// ❌ Boolean flags without messages\n{\n \"valid\": false,\n \"emailValid\": false,\n \"passwordValid\": false\n}\n```\n\n```javascript\n// ❌ Unhelpful validation response\napp.post('/users', (req, res) => {\n const errors = validate(req.body);\n if (errors.length > 0) {\n res.status(400).json({ error: 'Validation failed' });\n }\n});\n```\n\n**Problems:**\n- Clients cannot highlight specific form fields with their errors\n- Users must guess which fields need attention\n- Multiple form submissions required to discover all errors\n- Frontend validation libraries cannot map errors to form fields\n- No constraint context (e.g., min length, allowed range) for UI feedback\n\n## Correct\n\n```javascript\n// ✅ Detailed validation errors\nconst { body, validationResult } = require('express-validator');\n\nconst validateUser = [\n body('email')\n .notEmpty().withMessage('Email is required')\n .isEmail().withMessage('Must be a valid email address')\n .normalizeEmail(),\n\n body('password')\n .notEmpty().withMessage('Password is required')\n .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')\n .matches(/[A-Z]/).withMessage('Password must contain an uppercase letter')\n .matches(/[a-z]/).withMessage('Password must contain a lowercase letter')\n .matches(/[0-9]/).withMessage('Password must contain a number'),\n\n body('age')\n .optional()\n .isInt({ min: 0, max: 150 }).withMessage('Age must be between 0 and 150'),\n\n body('username')\n .notEmpty().withMessage('Username is required')\n .isLength({ min: 3, max: 30 }).withMessage('Username must be 3-30 characters')\n .matches(/^[a-zA-Z0-9_]+$/).withMessage('Username can only contain letters, numbers, and underscores')\n];\n\napp.post('/users', validateUser, (req, res) => {\n const errors = validationResult(req);\n\n if (!errors.isEmpty()) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'One or more fields have invalid values',\n details: errors.array().map(err => ({\n field: err.path,\n message: err.msg,\n value: err.value,\n location: err.location // body, query, params\n }))\n }\n });\n }\n\n // Create user...\n});\n```\n\n```json\n// ✅ Detailed validation error response\n{\n \"error\": {\n \"code\": \"validation_error\",\n \"message\": \"One or more fields have invalid values\",\n \"details\": [\n {\n \"field\": \"email\",\n \"message\": \"Must be a valid email address\",\n \"value\": \"not-an-email\",\n \"location\": \"body\"\n },\n {\n \"field\": \"password\",\n \"message\": \"Password must be at least 8 characters\",\n \"value\": \"short\",\n \"location\": \"body\",\n \"constraints\": {\n \"minLength\": 8,\n \"actualLength\": 5\n }\n },\n {\n \"field\": \"age\",\n \"message\": \"Age must be between 0 and 150\",\n \"value\": -5,\n \"location\": \"body\",\n \"constraints\": {\n \"min\": 0,\n \"max\": 150\n }\n }\n ]\n }\n}\n```\n\n```python\n# ✅ FastAPI with detailed validation\nfrom fastapi import FastAPI, HTTPException, Request\nfrom fastapi.exceptions import RequestValidationError\nfrom fastapi.responses import JSONResponse\nfrom pydantic import BaseModel, EmailStr, validator, Field\nfrom typing import Optional, List\n\napp = FastAPI()\n\nclass UserCreate(BaseModel):\n email: EmailStr\n password: str = Field(..., min_length=8, max_length=100)\n username: str = Field(..., min_length=3, max_length=30, regex=r'^[a-zA-Z0-9_]+

API Design Patterns RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation. Metadata - Version: 2.0.0 - Rule Count: 38 rules across 7 categories - License: MIT When to Apply Reference these guidelines when: - Designing new API endpoints - Reviewing existing API structure - Implementing error handling and validation - Setting up pagination, filtering, and sorting - Planning API versioning strategy - Configuring API securit…

)\n age: Optional[int] = Field(None, ge=0, le=150)\n\n @validator('password')\n def password_complexity(cls, v):\n errors = []\n if not any(c.isupper() for c in v):\n errors.append('must contain an uppercase letter')\n if not any(c.islower() for c in v):\n errors.append('must contain a lowercase letter')\n if not any(c.isdigit() for c in v):\n errors.append('must contain a number')\n if errors:\n raise ValueError(f\"Password {', '.join(errors)}\")\n return v\n\[email protected]_handler(RequestValidationError)\nasync def validation_exception_handler(request: Request, exc: RequestValidationError):\n details = []\n for error in exc.errors():\n field = '.'.join(str(loc) for loc in error['loc'] if loc != 'body')\n details.append({\n 'field': field,\n 'message': error['msg'],\n 'type': error['type'],\n 'context': error.get('ctx', {})\n })\n\n return JSONResponse(\n status_code=422,\n content={\n 'error': {\n 'code': 'validation_error',\n 'message': f'{len(details)} validation error(s) found',\n 'details': details\n }\n }\n )\n\[email protected](\"/users\")\nasync def create_user(user: UserCreate):\n return {\"id\": 1, **user.dict()}\n```\n\n```typescript\n// ✅ TypeScript/Zod validation with detailed errors\nimport { z } from 'zod';\nimport express from 'express';\n\nconst UserSchema = z.object({\n email: z.string()\n .min(1, 'Email is required')\n .email('Must be a valid email address'),\n\n password: z.string()\n .min(8, 'Password must be at least 8 characters')\n .regex(/[A-Z]/, 'Password must contain an uppercase letter')\n .regex(/[a-z]/, 'Password must contain a lowercase letter')\n .regex(/[0-9]/, 'Password must contain a number'),\n\n username: z.string()\n .min(3, 'Username must be at least 3 characters')\n .max(30, 'Username cannot exceed 30 characters')\n .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),\n\n age: z.number()\n .int('Age must be a whole number')\n .min(0, 'Age cannot be negative')\n .max(150, 'Age cannot exceed 150')\n .optional()\n});\n\napp.post('/users', (req, res) => {\n const result = UserSchema.safeParse(req.body);\n\n if (!result.success) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Validation failed',\n details: result.error.errors.map(err => ({\n field: err.path.join('.'),\n message: err.message,\n code: err.code\n }))\n }\n });\n }\n\n // Create user with result.data\n});\n```\n\n## Nested Object Validation\n\n```json\n// ✅ Validation errors for nested objects\n{\n \"error\": {\n \"code\": \"validation_error\",\n \"message\": \"Validation failed\",\n \"details\": [\n {\n \"field\": \"address.zipCode\",\n \"message\": \"ZIP code must be 5 digits\",\n \"value\": \"123\"\n },\n {\n \"field\": \"address.country\",\n \"message\": \"Country is required\"\n },\n {\n \"field\": \"contacts[0].email\",\n \"message\": \"Invalid email format\",\n \"value\": \"bad-email\"\n },\n {\n \"field\": \"contacts[1].phone\",\n \"message\": \"Phone number must include country code\",\n \"value\": \"555-1234\"\n }\n ]\n }\n}\n```\n\n**Benefits:**\n- Clients can highlight specific form fields with their errors\n- Users see exactly which fields need attention without guessing\n- Developers can quickly identify validation issues during development\n- Users can fix all issues at once instead of submitting multiple times\n- Frontend validation libraries can map errors directly to form fields\n- Clear validation responses help document expected input formats\n\nReference: [JSON:API Error Objects](https://jsonapi.org/format/#error-objects)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8088,"content_sha256":"21bea5e0125ecb617489aeeab2bdf3ac68d7f7c019d2a8483f3eb348170497e6"},{"filename":"rules/filter-query-params.md","content":"---\ntitle: Filter via Query Parameters\nimpact: HIGH\nimpactDescription: \"Cacheable, bookmarkable filtering that leverages HTTP semantics correctly\"\ntags: filtering, query-params, search\n---\n\n## Filter via Query Parameters\n\n**Impact: HIGH (Cacheable, bookmarkable filtering that leverages HTTP semantics correctly)**\n\nFiltering is a read operation and belongs in GET requests with query parameters. Using POST bodies for filtering breaks HTTP cacheability, makes URLs non-shareable, and violates the semantic contract of HTTP methods. Standard query parameters are combinable, cacheable, and immediately understandable.\n\n## Incorrect\n\n```http\nPOST /api/v1/users/search\nContent-Type: application/json\n\n{\n \"filters\": {\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_after\": \"2024-01-01\"\n }\n}\n```\n\n```http\n# Or: custom filter syntax that requires a parser\nGET /api/v1/users?filter=status:eq:active|role:eq:admin|created:gt:2024-01-01\n```\n\n**Problems:**\n- POST for read operations breaks HTTP caching at every layer (CDN, browser, proxy)\n- URLs cannot be bookmarked, shared, or logged meaningfully\n- Custom filter syntax requires client-side query builders and server-side parsers\n- Violates REST semantics — POST implies resource creation or mutation, not retrieval\n\n## Correct\n\n```http\nGET /api/v1/users?status=active&role=admin&created_after=2024-01-01\n```\n\n```json\n{\n \"data\": [\n {\n \"id\": 42,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_at\": \"2024-03-15T10:30:00Z\"\n }\n ],\n \"meta\": {\n \"total_count\": 12,\n \"filters_applied\": {\n \"status\": \"active\",\n \"role\": \"admin\",\n \"created_after\": \"2024-01-01\"\n }\n }\n}\n```\n\n```http\n# Multiple values for the same field (OR logic)\nGET /api/v1/users?status=active&status=pending\n\n# Range filters with clear suffixes\nGET /api/v1/orders?total_min=100&total_max=500&created_after=2024-01-01\n\n# Combine with search\nGET /api/v1/users?role=admin&q=smith\n```\n\n**Benefits:**\n- Fully cacheable by CDNs, reverse proxies, and browsers\n- URLs are bookmarkable and shareable — useful for dashboards and saved views\n- Filters are self-documenting and combinable with standard `&` syntax\n- No custom parser needed — standard query string parsing libraries handle it\n\nReference: [Google API Design Guide - Standard Methods](https://cloud.google.com/apis/design/standard_methods)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2433,"content_sha256":"12b04acdab824280333006f6453eaa02645ef9fac973b5108b9644f87455c5ee"},{"filename":"rules/page-consistent-params.md","content":"---\ntitle: Consistent Pagination Parameter Names\nimpact: HIGH\nimpactDescription: \"Reduces client integration time by 40-60% through predictable parameter conventions\"\ntags: pagination, parameters, consistency\n---\n\n## Consistent Pagination Parameter Names\n\n**Impact: HIGH (Reduces client integration time by 40-60% through predictable parameter conventions)**\n\nWhen different endpoints use different parameter names for pagination, every client integration becomes a special case. Developers waste time reading docs for each endpoint instead of applying one convention everywhere. Pick one style and enforce it across the entire API.\n\n## Incorrect\n\n```http\n# Users endpoint uses limit/offset\nGET /api/v1/users?limit=20&offset=40\n\n# Orders endpoint uses page/per_page\nGET /api/v1/orders?page=3&per_page=20\n\n# Products endpoint uses size/number\nGET /api/v1/products?size=20&number=3\n\n# Search endpoint uses count/start\nGET /api/v1/search?count=20&start=40\n```\n\n**Problems:**\n- Client SDKs need endpoint-specific pagination logic instead of a shared helper\n- Developers must consult documentation for every endpoint to find the right parameter names\n- Generic pagination UI components cannot be reused across different resource types\n- Increased chance of bugs when developers assume one convention but the endpoint uses another\n\n## Correct\n\n```http\n# Offset-based: use \"page\" + \"per_page\" everywhere\nGET /api/v1/users?page=3&per_page=20\nGET /api/v1/orders?page=3&per_page=20\nGET /api/v1/products?page=3&per_page=20\n\n# Cursor-based: use \"cursor\" + \"limit\" everywhere\nGET /api/v1/events?cursor=eyJpZCI6MTIzfQ&limit=20\nGET /api/v1/notifications?cursor=eyJpZCI6NDU2fQ&limit=20\nGET /api/v1/logs?cursor=eyJpZCI6Nzg5fQ&limit=20\n```\n\n**Benefits:**\n- One pagination helper in the client SDK handles all endpoints\n- Developers learn the convention once and apply it everywhere\n- Generic UI components (pagers, infinite scroll) work with any resource\n- API documentation is simpler — pagination is explained once, not per-endpoint\n\nReference: [Microsoft REST API Guidelines - Pagination](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#pagination)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2163,"content_sha256":"95f4167a00e35ef04ac5fc7e077ff667e8150a6c69b92f969c1d589a923f1e73"},{"filename":"rules/page-cursor-based.md","content":"---\ntitle: Cursor-Based Pagination for Large Datasets\nimpact: HIGH\nimpactDescription: \"Constant O(1) pagination performance regardless of dataset depth\"\ntags: pagination, cursor, performance, large-datasets\n---\n\n## Cursor-Based Pagination for Large Datasets\n\n**Impact: HIGH (Constant O(1) pagination performance regardless of dataset depth)**\n\nOffset pagination degrades linearly as page depth increases because the database must scan and discard all preceding rows. Cursor-based pagination uses an opaque pointer to the last retrieved item, enabling the database to seek directly to the next batch with constant performance.\n\n## Incorrect\n\n```http\nGET /api/v1/orders?offset=500000&limit=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 500001, \"total\": 49.99 },\n { \"id\": 500002, \"total\": 129.00 }\n ]\n}\n```\n\n```sql\n-- Behind the scenes: database scans 500,000 rows before returning 20\nSELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 500000;\n```\n\n**Problems:**\n- Query time grows linearly with offset — page 25,000 is dramatically slower than page 1\n- Database must scan and discard all rows before the offset\n- Inconsistent results if rows are inserted or deleted between page requests\n- Memory and CPU waste on large tables (millions of rows)\n\n## Correct\n\n```http\nGET /api/v1/orders?cursor=eyJpZCI6NTAwMDAwfQ&limit=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 500001, \"total\": 49.99 },\n { \"id\": 500002, \"total\": 129.00 }\n ],\n \"meta\": {\n \"has_more\": true,\n \"next_cursor\": \"eyJpZCI6NTAwMDIwfQ\"\n },\n \"links\": {\n \"next\": \"/api/v1/orders?cursor=eyJpZCI6NTAwMDIwfQ&limit=20\"\n }\n}\n```\n\n```sql\n-- Behind the scenes: index seek, constant performance\nSELECT * FROM orders WHERE id > 500000 ORDER BY id LIMIT 20;\n```\n\n**Benefits:**\n- Constant query time regardless of how deep into the dataset the client has paginated\n- Stable results — no skipped or duplicated items when data changes between requests\n- Efficient use of database indexes (seek instead of scan)\n- Opaque cursor allows server-side implementation changes without breaking clients\n\nReference: [Slack API - Pagination](https://api.slack.com/docs/pagination)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2128,"content_sha256":"1c5521dcec3420098a8c93e2778109ffbdc8c3c6b66bef6469c979eba29757e1"},{"filename":"rules/page-metadata.md","content":"---\ntitle: Include Pagination Metadata in Responses\nimpact: HIGH\nimpactDescription: \"Eliminates guesswork pagination and reduces unnecessary API calls by 30-50%\"\ntags: pagination, metadata, total-count\n---\n\n## Include Pagination Metadata in Responses\n\n**Impact: HIGH (Eliminates guesswork pagination and reduces unnecessary API calls by 30-50%)**\n\nWithout pagination metadata, clients must make an extra request to discover there are no more results, or blindly paginate until they receive an empty response. Including metadata in every paginated response gives clients everything they need to render UI controls and make efficient decisions about fetching more data.\n\n## Incorrect\n\n```http\nGET /api/v1/products?page=2&per_page=20\n```\n\n```json\n[\n { \"id\": 21, \"name\": \"Widget A\", \"price\": 9.99 },\n { \"id\": 22, \"name\": \"Widget B\", \"price\": 14.99 }\n]\n```\n\n**Problems:**\n- Client cannot distinguish \"page has fewer items than per_page\" from \"this is the last page\"\n- No total count means page selector UIs and \"X results found\" labels are impossible\n- Client must request the next page to discover it is empty — wasting a round trip\n- No navigation links forces clients to manually construct pagination URLs\n\n## Correct\n\n```http\nGET /api/v1/products?page=2&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 21, \"name\": \"Widget A\", \"price\": 9.99 },\n { \"id\": 22, \"name\": \"Widget B\", \"price\": 14.99 }\n ],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 20,\n \"total_count\": 195,\n \"total_pages\": 10,\n \"has_more\": true\n },\n \"links\": {\n \"first\": \"/api/v1/products?page=1&per_page=20\",\n \"prev\": \"/api/v1/products?page=1&per_page=20\",\n \"next\": \"/api/v1/products?page=3&per_page=20\",\n \"last\": \"/api/v1/products?page=10&per_page=20\"\n }\n}\n```\n\n**Benefits:**\n- `has_more` lets infinite-scroll UIs know when to stop fetching without an extra empty request\n- `total_count` and `total_pages` enable \"Showing 21-40 of 195 results\" display\n- Navigation links let clients follow links instead of constructing URLs, reducing coupling\n- Consistent envelope structure makes client-side deserialization predictable across all endpoints\n\nReference: [GitHub REST API - Pagination](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2273,"content_sha256":"412625c6811944e9f0e0379699a04ca11a1cc08fdbb18edb705fc61502d36a56"},{"filename":"rules/page-offset-based.md","content":"---\ntitle: Offset Pagination for Simple Cases\nimpact: HIGH\nimpactDescription: \"Enables random page access and predictable navigation for small-to-medium datasets\"\ntags: pagination, offset, page-number\n---\n\n## Offset Pagination for Simple Cases\n\n**Impact: HIGH (Enables random page access and predictable navigation for small-to-medium datasets)**\n\nOffset-based pagination is the most intuitive model for clients that need numbered pages, total counts, and the ability to jump to arbitrary pages. It works well for small-to-medium datasets and should always include metadata and navigation links so clients never have to guess the pagination state.\n\n## Incorrect\n\n```http\nGET /api/v1/articles?page=2\n```\n\n```json\n[\n { \"id\": 21, \"title\": \"Introduction to REST\" },\n { \"id\": 22, \"title\": \"API Versioning\" }\n]\n```\n\n**Problems:**\n- Bare array provides no pagination context — client cannot determine total pages or current position\n- No navigation links — client must construct URLs manually and guess when to stop\n- No indication of page size — unclear how many items per page the server returned\n- Client cannot build a page selector UI without total count information\n\n## Correct\n\n```http\nGET /api/v1/articles?page=2&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 21, \"title\": \"Introduction to REST\" },\n { \"id\": 22, \"title\": \"API Versioning\" }\n ],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 20,\n \"total_pages\": 10,\n \"total_count\": 195\n },\n \"links\": {\n \"first\": \"/api/v1/articles?page=1&per_page=20\",\n \"prev\": \"/api/v1/articles?page=1&per_page=20\",\n \"next\": \"/api/v1/articles?page=3&per_page=20\",\n \"last\": \"/api/v1/articles?page=10&per_page=20\"\n }\n}\n```\n\n**Benefits:**\n- Full pagination envelope lets clients render page selectors, \"showing X of Y\" indicators, and navigation controls\n- Navigation links follow HATEOAS principles — clients follow links rather than constructing URLs\n- `prev` and `next` are null-safe (omitted on first and last pages respectively)\n- `per_page` parameter gives clients control over batch size within server-enforced limits\n\nReference: [JSON:API - Pagination](https://jsonapi.org/format/#fetching-pagination)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2183,"content_sha256":"16790e308842e36e072fee0202efad51e851c70ad92ffcf95a1d8c7937282531"},{"filename":"rules/resp-compression.md","content":"---\ntitle: Response Compression\nimpact: MEDIUM\nimpactDescription: \"Reduces JSON payload transfer size by 60-80%\"\ntags: compression, gzip, performance, bandwidth\n---\n\n## Response Compression\n\n**Impact: MEDIUM (Reduces JSON payload transfer size by 60-80%)**\n\nJSON is highly compressible because of its repetitive structure (keys, braces, quotes). Enabling compression is one of the simplest performance wins for any API, dramatically reducing transfer times with minimal CPU overhead.\n\n## Incorrect\n\n```http\n// ❌ No compression — client doesn't advertise, server doesn't compress\nGET /api/users?per_page=100 HTTP/1.1\nHost: api.example.com\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 245760\n\n// 240 KB uncompressed JSON transferred over the wire\n{\n \"data\": [\n { \"id\": 1, \"name\": \"Jane Doe\", \"email\": \"[email protected]\", ... },\n { \"id\": 2, \"name\": \"John Smith\", \"email\": \"[email protected]\", ... },\n // ... 98 more records\n ]\n}\n```\n\n**Problems:**\n- 240 KB transferred when 48 KB (gzip) or 38 KB (Brotli) would suffice\n- Slower time-to-first-byte, especially on mobile connections\n- Higher bandwidth costs for both server and client\n- Poor experience for users on metered or slow networks\n\n## Correct\n\n### Client Requests Compression\n\n```http\n// ✅ Client advertises supported encodings\nGET /api/users?per_page=100 HTTP/1.1\nHost: api.example.com\nAccept-Encoding: gzip, br\n```\n\n### Server Responds with Compressed Content\n\n```http\n// ✅ gzip compression — widely supported\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Encoding: gzip\nVary: Accept-Encoding\nContent-Length: 48200\n\n// Same 240 KB JSON, now 48 KB over the wire (80% reduction)\n```\n\n```http\n// ✅ Brotli compression — better ratio for modern clients\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Encoding: br\nVary: Accept-Encoding\nContent-Length: 38400\n\n// Same 240 KB JSON, now 38 KB over the wire (84% reduction)\n```\n\n### Typical Compression Ratios for JSON\n\n```\nPayload Type | Raw | gzip | Brotli | gzip % | Brotli %\n-----------------------+---------+---------+---------+---------+---------\nSmall object (1 KB) | 1 KB | 0.5 KB | 0.4 KB | 50% | 60%\nList of 100 records | 240 KB | 48 KB | 38 KB | 80% | 84%\nLarge nested response | 1.2 MB | 180 KB | 140 KB | 85% | 88%\nPaginated collection | 500 KB | 85 KB | 65 KB | 83% | 87%\n```\n\n### Important Headers\n\n```http\n// ✅ Always include Vary header so caches store compressed\n// and uncompressed versions separately\nVary: Accept-Encoding\n\n// ✅ Set minimum size threshold — don't compress tiny responses\n// Most servers skip compression for responses under 1 KB\n```\n\n### Nginx Configuration Example\n\n```nginx\n# Enable gzip compression\ngzip on;\ngzip_types application/json application/javascript text/plain;\ngzip_min_length 1024;\ngzip_comp_level 6;\ngzip_vary on;\n\n# Enable Brotli (if module installed)\nbrotli on;\nbrotli_types application/json application/javascript text/plain;\nbrotli_min_length 1024;\nbrotli_comp_level 6;\n```\n\n### curl — Verifying Compression\n\n```bash\n# Request with compression and see headers\ncurl -s -H \"Accept-Encoding: gzip, br\" \\\n -D - -o /dev/null \\\n https://api.example.com/users\n\n# Decompress and inspect\ncurl -s -H \"Accept-Encoding: gzip\" \\\n --compressed \\\n https://api.example.com/users | jq .\n```\n\n**Benefits:**\n- 60-80% bandwidth reduction with gzip, 70-88% with Brotli\n- Faster response delivery, especially over high-latency connections\n- Lower bandwidth costs at scale\n- Negligible CPU overhead on modern hardware (1-2% for gzip level 6)\n- Transparent to clients — `curl --compressed` and all HTTP libraries handle it automatically\n\nReference: [MDN — Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3829,"content_sha256":"55477b96b270812c120e34afe250db981a2faf0c7e136c12d5001e8e89de29c0"},{"filename":"rules/resp-consistent-structure.md","content":"---\ntitle: Consistent Response Envelope\nimpact: MEDIUM\nimpactDescription: \"Reduces client-side parsing complexity by 40-60%\"\ntags: response, envelope, structure, consistency\n---\n\n## Consistent Response Envelope\n\n**Impact: MEDIUM (Reduces client-side parsing complexity by 40-60%)**\n\nA consistent response envelope allows API consumers to build reusable parsing logic that works across every endpoint. Without it, clients must special-case each endpoint's response shape, leading to fragile integration code and slower onboarding.\n\n## Incorrect\n\n```json\n// ❌ Different shapes for different endpoints\n\n// GET /users/123 — bare object\n{\n \"id\": 123,\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n}\n\n// GET /users — bare array\n[\n { \"id\": 123, \"name\": \"Jane Doe\" },\n { \"id\": 456, \"name\": \"John Smith\" }\n]\n\n// GET /orders — nested differently\n{\n \"orders\": [\n { \"id\": 1, \"total\": 99.99 }\n ],\n \"count\": 1\n}\n\n// GET /products/42 — yet another shape\n{\n \"product\": {\n \"id\": 42,\n \"title\": \"Widget\"\n },\n \"status\": \"ok\"\n}\n```\n\n**Problems:**\n- Clients cannot predict the response structure for new endpoints\n- Every endpoint requires unique parsing logic\n- Impossible to build a generic API client or SDK\n- Adding metadata (pagination, rate limits) requires breaking changes\n\n## Correct\n\n### Simple Envelope Style\n\n```json\n// ✅ Single resource — GET /users/123\n{\n \"data\": {\n \"id\": 123,\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n },\n \"meta\": {\n \"request_id\": \"req_abc123\"\n }\n}\n\n// ✅ Collection — GET /users?page=1&per_page=20\n{\n \"data\": [\n { \"id\": 123, \"name\": \"Jane Doe\" },\n { \"id\": 456, \"name\": \"John Smith\" }\n ],\n \"meta\": {\n \"page\": 1,\n \"per_page\": 20,\n \"total\": 142,\n \"total_pages\": 8,\n \"request_id\": \"req_def456\"\n }\n}\n\n// ✅ Empty collection — GET /users?status=banned\n{\n \"data\": [],\n \"meta\": {\n \"page\": 1,\n \"per_page\": 20,\n \"total\": 0,\n \"total_pages\": 0,\n \"request_id\": \"req_ghi789\"\n }\n}\n```\n\n### JSON:API Style\n\n```json\n// ✅ Single resource — GET /users/123\n{\n \"data\": {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": {\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\"\n },\n \"relationships\": {\n \"company\": {\n \"data\": { \"type\": \"companies\", \"id\": \"7\" }\n }\n }\n },\n \"included\": [\n {\n \"type\": \"companies\",\n \"id\": \"7\",\n \"attributes\": {\n \"name\": \"Acme Corp\"\n }\n }\n ]\n}\n\n// ✅ Collection — GET /users\n{\n \"data\": [\n {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": { \"name\": \"Jane Doe\" }\n },\n {\n \"type\": \"users\",\n \"id\": \"456\",\n \"attributes\": { \"name\": \"John Smith\" }\n }\n ],\n \"meta\": {\n \"total\": 142,\n \"page\": 1,\n \"per_page\": 20\n },\n \"links\": {\n \"self\": \"/users?page=1\",\n \"next\": \"/users?page=2\",\n \"last\": \"/users?page=8\"\n }\n}\n```\n\n**Benefits:**\n- Clients build one parser that works for every endpoint\n- Metadata (pagination, request IDs, rate limits) has a predictable location\n- New metadata can be added to `meta` without breaking existing clients\n- SDKs and generic API wrappers become straightforward to implement\n\nReference: [JSON:API Specification](https://jsonapi.org/format/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3234,"content_sha256":"00dc393dd13f5c5ccff2fe071b4ace3b7287b49b0660b2ca51578397ee191623"},{"filename":"rules/resp-json-conventions.md","content":"---\ntitle: JSON Naming Conventions\nimpact: MEDIUM\nimpactDescription: \"Eliminates field-name guessing and mapping errors\"\ntags: json, naming, camelCase, snake_case\n---\n\n## JSON Naming Conventions\n\n**Impact: MEDIUM (Eliminates field-name guessing and mapping errors)**\n\nInconsistent naming forces developers to guess field names and write tedious mapping code. Picking one convention and applying it everywhere makes the API predictable and reduces integration bugs.\n\n## Incorrect\n\n```json\n// ❌ Mixed conventions in the same response\n{\n \"userId\": 123,\n \"first_name\": \"Jane\",\n \"LastName\": \"Doe\",\n \"Email\": \"[email protected]\",\n \"created_at\": \"2024-01-15\",\n \"lastLogin\": \"Jan 20, 2024 3:45 PM\",\n \"isActive\": true,\n \"acct_type\": \"premium\",\n \"DOB\": \"1990-05-20\",\n \"addr\": {\n \"str\": \"123 Main St\",\n \"ZipCode\": \"90210\"\n }\n}\n```\n\n**Problems:**\n- Developers cannot predict whether a field uses camelCase, snake_case, or PascalCase\n- Abbreviations like `acct`, `str`, `DOB` are ambiguous\n- Date formats vary across fields, requiring per-field parsing\n- Mapping between API responses and client models becomes error-prone\n\n## Correct\n\n### snake_case (common for Ruby, Python, PHP APIs)\n\n```json\n// ✅ Consistent snake_case\n{\n \"user_id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"created_at\": \"2024-01-15T10:30:00Z\",\n \"last_login_at\": \"2024-01-20T15:45:00Z\",\n \"is_active\": true,\n \"account_type\": \"premium\",\n \"date_of_birth\": \"1990-05-20\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"zip_code\": \"90210\"\n }\n}\n```\n\n### camelCase (common for JavaScript/TypeScript APIs)\n\n```json\n// ✅ Consistent camelCase\n{\n \"userId\": 123,\n \"firstName\": \"Jane\",\n \"lastName\": \"Doe\",\n \"email\": \"[email protected]\",\n \"createdAt\": \"2024-01-15T10:30:00Z\",\n \"lastLoginAt\": \"2024-01-20T15:45:00Z\",\n \"isActive\": true,\n \"accountType\": \"premium\",\n \"dateOfBirth\": \"1990-05-20\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"zipCode\": \"90210\"\n }\n}\n```\n\n### Date and Time — Always ISO 8601\n\n```json\n// ✅ ISO 8601 with timezone\n{\n \"created_at\": \"2024-01-15T10:30:00Z\",\n \"updated_at\": \"2024-03-01T14:22:33+05:30\",\n \"expires_on\": \"2024-12-31\",\n \"duration_seconds\": 3600\n}\n\n// ❌ Avoid non-standard date formats\n{\n \"created_at\": \"Jan 15, 2024\",\n \"updated_at\": \"03/01/2024\",\n \"expires_on\": \"1704067200\"\n}\n```\n\n### Null vs Omitted Fields\n\n```json\n// ✅ Use null for \"set but empty\" — include the field\n{\n \"first_name\": \"Jane\",\n \"middle_name\": null,\n \"last_name\": \"Doe\"\n}\n\n// ✅ Omit fields that don't apply to this resource\n// A \"company\" user has a company_name; a \"personal\" user omits it\n{\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"account_type\": \"personal\"\n}\n```\n\n### Boolean Naming\n\n```json\n// ✅ Use is_, has_, can_, should_ prefixes\n{\n \"is_active\": true,\n \"is_verified\": false,\n \"has_two_factor\": true,\n \"can_edit\": false,\n \"should_notify\": true\n}\n\n// ❌ Ambiguous boolean names\n{\n \"active\": true,\n \"verified\": 1,\n \"two_factor\": \"yes\",\n \"edit\": false,\n \"notification\": true\n}\n```\n\n**Benefits:**\n- Developers can predict any field name without checking the docs\n- Automated serialization/deserialization works without custom mappings\n- ISO 8601 dates are natively parseable in every language\n- Boolean prefixes make the type and intent immediately clear\n\nReference: [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3427,"content_sha256":"1496167007aee29c7361d49db1d4a11c167234abc3527391c9426da64ae27405"},{"filename":"rules/resp-partial-responses.md","content":"---\ntitle: Field Selection (Sparse Fieldsets)\nimpact: MEDIUM\nimpactDescription: \"Reduces payload size by 50-90% for field-heavy resources\"\ntags: fields, sparse-fieldsets, partial-response, bandwidth\n---\n\n## Field Selection (Sparse Fieldsets)\n\n**Impact: MEDIUM (Reduces payload size by 50-90% for field-heavy resources)**\n\nWhen an endpoint returns 50+ fields but the client only needs 3, the wasted bandwidth slows mobile apps, increases server serialization time, and drives up data transfer costs. Field selection lets clients request only what they need.\n\n## Incorrect\n\n```http\n// ❌ Client only needs name and avatar, but gets everything\nGET /api/users/123\n\n// Response: 2.4 KB\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"email\": \"[email protected]\",\n \"phone\": \"+1-555-0100\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\",\n \"date_of_birth\": \"1990-05-20\",\n \"bio\": \"Software engineer with 10 years of experience...\",\n \"address\": {\n \"street\": \"123 Main St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip_code\": \"62701\",\n \"country\": \"US\"\n },\n \"preferences\": {\n \"language\": \"en\",\n \"timezone\": \"America/Chicago\",\n \"theme\": \"dark\",\n \"notifications\": {\n \"email\": true,\n \"sms\": false,\n \"push\": true\n }\n },\n \"social_links\": {\n \"twitter\": \"https://twitter.com/janedoe\",\n \"linkedin\": \"https://linkedin.com/in/janedoe\",\n \"github\": \"https://github.com/janedoe\"\n },\n \"created_at\": \"2023-01-15T10:30:00Z\",\n \"updated_at\": \"2024-03-01T14:22:33Z\",\n \"last_login_at\": \"2024-03-10T09:15:00Z\"\n }\n}\n```\n\n**Problems:**\n- Wasted bandwidth — client discards 90% of the response\n- Slower responses on mobile or low-bandwidth connections\n- Server serializes and queries for unused data\n- Higher data transfer costs at scale\n\n## Correct\n\n### Query Parameter Approach\n\n```http\n// ✅ Client requests only the fields it needs\nGET /api/users/123?fields=id,first_name,last_name,avatar_url\n\n// Response: 180 bytes (93% smaller)\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"last_name\": \"Doe\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\"\n }\n}\n```\n\n### Nested Fields\n\n```http\n// ✅ Dot notation for nested field selection\nGET /api/users/123?fields=id,first_name,address.city,address.country\n\n{\n \"data\": {\n \"id\": 123,\n \"first_name\": \"Jane\",\n \"address\": {\n \"city\": \"Springfield\",\n \"country\": \"US\"\n }\n }\n}\n```\n\n### Collections with Field Selection\n\n```http\n// ✅ Sparse fieldsets on collections — big savings at scale\nGET /api/users?fields=id,first_name,avatar_url&per_page=50\n\n// Instead of 50 × 2.4 KB = 120 KB\n// Now 50 × 120 bytes = 6 KB\n{\n \"data\": [\n { \"id\": 123, \"first_name\": \"Jane\", \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\" },\n { \"id\": 456, \"first_name\": \"John\", \"avatar_url\": \"https://cdn.example.com/avatars/456.jpg\" }\n ],\n \"meta\": {\n \"total\": 142,\n \"page\": 1,\n \"per_page\": 50\n }\n}\n```\n\n### JSON:API Sparse Fieldsets\n\n```http\n// ✅ JSON:API uses typed field selection\nGET /api/users?fields[users]=first_name,avatar_url&fields[companies]=name\n\n{\n \"data\": [\n {\n \"type\": \"users\",\n \"id\": \"123\",\n \"attributes\": {\n \"first_name\": \"Jane\",\n \"avatar_url\": \"https://cdn.example.com/avatars/123.jpg\"\n },\n \"relationships\": {\n \"company\": { \"data\": { \"type\": \"companies\", \"id\": \"7\" } }\n }\n }\n ],\n \"included\": [\n {\n \"type\": \"companies\",\n \"id\": \"7\",\n \"attributes\": { \"name\": \"Acme Corp\" }\n }\n ]\n}\n```\n\n**Benefits:**\n- Payload size drops 50-90% for typical use cases\n- Faster responses, especially on mobile networks\n- Server can optimize database queries to fetch only requested columns\n- Reduced data transfer costs at high request volumes\n\nReference: [Google API Design Guide — Standard Fields](https://cloud.google.com/apis/design/standard_fields)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3966,"content_sha256":"175f158a44aa80f0059334b2017f2a5165af90a66694ff30e6fc758eb48975be"},{"filename":"rules/rest-hateoas.md","content":"---\ntitle: Include HATEOAS Links for Discoverability\nimpact: CRITICAL\nimpactDescription: \"Improves API discoverability and reduces client coupling\"\ntags: rest, hateoas, hypermedia, discoverability\n---\n\n## Include HATEOAS Links for Discoverability\n\n**Impact: CRITICAL (Improves API discoverability and reduces client coupling)**\n\nHATEOAS (Hypermedia as the Engine of Application State) provides links in responses that guide clients to related resources and available actions.\n\n## Incorrect\n\n```json\n// ❌ No links, client must construct URLs\n{\n \"id\": 123,\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"orderId\": 456\n}\n// Client must know to call GET /orders/456 to get order details\n// No indication of available actions\n```\n\n```javascript\n// ❌ Response without navigation\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json(user); // Raw data only\n});\n```\n\n**Problems:**\n- Clients must hardcode URL patterns, creating tight coupling\n- No indication of what actions are available on a resource\n- API URL changes break all clients\n- New features are not automatically discoverable\n- Clients cannot adapt behavior based on resource state\n\n## Correct\n\n```javascript\n// ✅ Response with HATEOAS links\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n const baseUrl = `${req.protocol}://${req.get('host')}`;\n\n res.json({\n id: user.id,\n name: user.name,\n email: user.email,\n _links: {\n self: {\n href: `${baseUrl}/users/${user.id}`,\n method: 'GET'\n },\n update: {\n href: `${baseUrl}/users/${user.id}`,\n method: 'PUT'\n },\n delete: {\n href: `${baseUrl}/users/${user.id}`,\n method: 'DELETE'\n },\n orders: {\n href: `${baseUrl}/users/${user.id}/orders`,\n method: 'GET'\n },\n createOrder: {\n href: `${baseUrl}/users/${user.id}/orders`,\n method: 'POST'\n }\n }\n });\n});\n\n// ✅ Collection with pagination links\napp.get('/users', async (req, res) => {\n const page = parseInt(req.query.page) || 1;\n const limit = parseInt(req.query.limit) || 20;\n const { users, total } = await db.findUsers({ page, limit });\n const baseUrl = `${req.protocol}://${req.get('host')}`;\n const totalPages = Math.ceil(total / limit);\n\n res.json({\n data: users.map(user => ({\n ...user,\n _links: {\n self: { href: `${baseUrl}/users/${user.id}` }\n }\n })),\n _links: {\n self: { href: `${baseUrl}/users?page=${page}&limit=${limit}` },\n first: { href: `${baseUrl}/users?page=1&limit=${limit}` },\n last: { href: `${baseUrl}/users?page=${totalPages}&limit=${limit}` },\n ...(page > 1 && {\n prev: { href: `${baseUrl}/users?page=${page - 1}&limit=${limit}` }\n }),\n ...(page \u003c totalPages && {\n next: { href: `${baseUrl}/users?page=${page + 1}&limit=${limit}` }\n })\n },\n _meta: {\n currentPage: page,\n totalPages,\n totalItems: total,\n itemsPerPage: limit\n }\n });\n});\n```\n\n```json\n// ✅ Example response with HATEOAS\n{\n \"id\": 123,\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"status\": \"active\",\n \"_links\": {\n \"self\": {\n \"href\": \"https://api.example.com/users/123\",\n \"method\": \"GET\"\n },\n \"update\": {\n \"href\": \"https://api.example.com/users/123\",\n \"method\": \"PUT\"\n },\n \"deactivate\": {\n \"href\": \"https://api.example.com/users/123/deactivate\",\n \"method\": \"POST\"\n },\n \"orders\": {\n \"href\": \"https://api.example.com/users/123/orders\",\n \"method\": \"GET\"\n },\n \"avatar\": {\n \"href\": \"https://api.example.com/users/123/avatar\",\n \"method\": \"GET\",\n \"type\": \"image/png\"\n }\n },\n \"_embedded\": {\n \"latestOrder\": {\n \"id\": 456,\n \"total\": 99.99,\n \"_links\": {\n \"self\": { \"href\": \"https://api.example.com/orders/456\" }\n }\n }\n }\n}\n```\n\n```python\n# ✅ FastAPI with HATEOAS helper\nfrom fastapi import FastAPI, Request\nfrom pydantic import BaseModel\nfrom typing import Dict, List, Optional, Any\n\napp = FastAPI()\n\nclass Link(BaseModel):\n href: str\n method: str = \"GET\"\n type: Optional[str] = None\n\nclass HATEOASResponse(BaseModel):\n data: Any\n _links: Dict[str, Link]\n _embedded: Optional[Dict[str, Any]] = None\n\ndef build_user_links(request: Request, user_id: int) -> Dict[str, Link]:\n base_url = str(request.base_url).rstrip('/')\n return {\n \"self\": Link(href=f\"{base_url}/users/{user_id}\"),\n \"update\": Link(href=f\"{base_url}/users/{user_id}\", method=\"PUT\"),\n \"delete\": Link(href=f\"{base_url}/users/{user_id}\", method=\"DELETE\"),\n \"orders\": Link(href=f\"{base_url}/users/{user_id}/orders\"),\n }\n\[email protected](\"/users/{user_id}\")\nasync def get_user(user_id: int, request: Request):\n user = await db.get_user(user_id)\n return {\n **user.dict(),\n \"_links\": build_user_links(request, user_id)\n }\n\[email protected](\"/orders/{order_id}\")\nasync def get_order(order_id: int, request: Request):\n order = await db.get_order(order_id)\n base_url = str(request.base_url).rstrip('/')\n\n cancel_link = (\n {\"href\": f\"{base_url}/orders/{order_id}/cancel\", \"method\": \"POST\"}\n if order.status == \"pending\"\n else None\n )\n\n return {\n **order.dict(),\n \"_links\": {\n \"self\": {\"href\": f\"{base_url}/orders/{order_id}\"},\n \"customer\": {\"href\": f\"{base_url}/users/{order.customer_id}\"},\n \"items\": {\"href\": f\"{base_url}/orders/{order_id}/items\"},\n \"cancel\": cancel_link,\n \"invoice\": {\"href\": f\"{base_url}/orders/{order_id}/invoice\", \"type\": \"application/pdf\"}\n }\n }\n```\n\n## HAL Format (Common Standard)\n\n```json\n{\n \"_links\": {\n \"self\": { \"href\": \"/orders/123\" },\n \"customer\": { \"href\": \"/customers/456\", \"title\": \"John Doe\" },\n \"items\": { \"href\": \"/orders/123/items\" }\n },\n \"id\": 123,\n \"total\": 99.99,\n \"status\": \"shipped\",\n \"_embedded\": {\n \"items\": [\n {\n \"_links\": { \"self\": { \"href\": \"/products/789\" } },\n \"name\": \"Widget\",\n \"quantity\": 2\n }\n ]\n }\n}\n```\n\n**Benefits:**\n- Responses tell clients exactly what actions are available and how to perform them\n- Clients follow links dynamically instead of hardcoding URL patterns\n- APIs can change URL structures without breaking clients\n- New features are automatically discoverable through new links\n- Links can vary based on resource state (e.g., \"cancel\" only for pending orders)\n- Links guide users through multi-step processes naturally\n\nReference: [HAL Specification](https://stateless.group/hal_specification.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6680,"content_sha256":"74eb610f4eea051e62f1f2c24148e2c940c6bc15addebbebd32b438920b8d86e"},{"filename":"rules/rest-http-methods.md","content":"---\ntitle: Use HTTP Methods Correctly\nimpact: CRITICAL\nimpactDescription: \"Enables caching, retry logic, and semantic API operations\"\ntags: rest, http-methods, idempotency, safety\n---\n\n## Use HTTP Methods Correctly\n\n**Impact: CRITICAL (Enables caching, retry logic, and semantic API operations)**\n\nHTTP methods have specific semantics and should be used according to their intended purpose. Each method has distinct characteristics for safety and idempotency.\n\n## Incorrect\n\n```json\n// ❌ Incorrect method usage\nPOST /users/123 // Should use GET to retrieve\nGET /users/create // Should use POST to create\nPOST /users/123/delete // Should use DELETE method\nGET /orders/123/update // Should use PUT/PATCH\nPOST /search // GET is better for safe operations\n```\n\n```javascript\n// ❌ Incorrect method usage\napp.post('/users/:id', (req, res) => {\n // Fetching user with POST - wrong!\n const user = db.findUser(req.params.id);\n res.json(user);\n});\n\napp.get('/users/delete/:id', (req, res) => {\n // Deleting with GET - dangerous!\n db.deleteUser(req.params.id);\n res.json({ deleted: true });\n});\n\napp.post('/users/:id/update', (req, res) => {\n // Custom action verb with POST\n db.updateUser(req.params.id, req.body);\n res.json({ updated: true });\n});\n```\n\n**Problems:**\n- GET requests that modify data can be triggered by crawlers or link prefetchers\n- POST for retrieval prevents browser and CDN caching\n- Clients cannot implement safe retry logic without correct method semantics\n- Security tools and load balancers cannot apply method-based rules\n- API documentation tools generate incorrect specifications\n\n## Correct\n\n```javascript\n// ✅ Correct HTTP method usage\nconst express = require('express');\nconst router = express.Router();\n\n// GET - Retrieve resource(s), safe and idempotent\nrouter.get('/users', async (req, res) => {\n const users = await db.findUsers(req.query);\n res.json(users);\n});\n\nrouter.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n if (!user) return res.status(404).json({ error: 'User not found' });\n res.json(user);\n});\n\n// POST - Create new resource, not idempotent\nrouter.post('/users', async (req, res) => {\n const user = await db.createUser(req.body);\n res.status(201).json(user);\n});\n\n// PUT - Replace entire resource, idempotent\nrouter.put('/users/:id', async (req, res) => {\n const user = await db.replaceUser(req.params.id, req.body);\n res.json(user);\n});\n\n// PATCH - Partial update, not necessarily idempotent\nrouter.patch('/users/:id', async (req, res) => {\n const user = await db.updateUser(req.params.id, req.body);\n res.json(user);\n});\n\n// DELETE - Remove resource, idempotent\nrouter.delete('/users/:id', async (req, res) => {\n await db.deleteUser(req.params.id);\n res.status(204).send();\n});\n\n// HEAD - Same as GET but no body, for checking existence\nrouter.head('/users/:id', async (req, res) => {\n const exists = await db.userExists(req.params.id);\n res.status(exists ? 200 : 404).send();\n});\n\n// OPTIONS - Return allowed methods\nrouter.options('/users', (req, res) => {\n res.set('Allow', 'GET, POST, OPTIONS');\n res.status(204).send();\n});\n```\n\n```python\n# ✅ FastAPI with correct HTTP methods\nfrom fastapi import FastAPI, HTTPException, status\n\napp = FastAPI()\n\n# GET - Retrieve\[email protected](\"/users\")\ndef list_users(skip: int = 0, limit: int = 10):\n return db.get_users(skip=skip, limit=limit)\n\[email protected](\"/users/{user_id}\")\ndef get_user(user_id: int):\n user = db.get_user(user_id)\n if not user:\n raise HTTPException(status_code=404, detail=\"User not found\")\n return user\n\n# POST - Create\[email protected](\"/users\", status_code=status.HTTP_201_CREATED)\ndef create_user(user: UserCreate):\n return db.create_user(user)\n\n# PUT - Full replacement\[email protected](\"/users/{user_id}\")\ndef replace_user(user_id: int, user: UserUpdate):\n return db.replace_user(user_id, user)\n\n# PATCH - Partial update\[email protected](\"/users/{user_id}\")\ndef update_user(user_id: int, user: UserPatch):\n return db.update_user(user_id, user.dict(exclude_unset=True))\n\n# DELETE - Remove\[email protected](\"/users/{user_id}\", status_code=status.HTTP_204_NO_CONTENT)\ndef delete_user(user_id: int):\n db.delete_user(user_id)\n return None\n```\n\n## HTTP Methods Reference\n\n| Method | Purpose | Safe | Idempotent | Request Body | Response Body |\n|---------|---------|------|------------|--------------|---------------|\n| GET | Retrieve | Yes | Yes | No | Yes |\n| POST | Create | No | No | Yes | Yes |\n| PUT | Replace | No | Yes | Yes | Yes |\n| PATCH | Update | No | Not guaranteed | Yes | Yes |\n| DELETE | Remove | No | Yes | Optional | Optional |\n| HEAD | Headers | Yes | Yes | No | No |\n| OPTIONS | Methods | Yes | Yes | No | No |\n\n**Benefits:**\n- Each method has clear, well-defined purpose all developers understand\n- GET requests can be cached by browsers and CDNs\n- Browsers handle different methods appropriately (e.g., warn before resubmitting POST)\n- Security tools, load balancers, and proxies understand HTTP semantics\n- Idempotent methods (GET, PUT, DELETE) can be safely retried on network failures\n- Tools like Swagger/OpenAPI rely on correct method usage for accurate documentation\n\nReference: [MDN HTTP Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5500,"content_sha256":"3195682c43a015d23d93bdaded0b31d3bc511b2fcba85acfb491ee9052689111"},{"filename":"rules/rest-idempotency.md","content":"---\ntitle: Implement Idempotency for Safe Retries\nimpact: CRITICAL\nimpactDescription: \"Prevents duplicate operations and enables safe retries\"\ntags: rest, idempotency, reliability, retries\n---\n\n## Implement Idempotency for Safe Retries\n\n**Impact: CRITICAL (Prevents duplicate operations and enables safe retries)**\n\nIdempotent operations produce the same result regardless of how many times they're executed. Implement idempotency keys for non-idempotent operations to enable safe retries.\n\n## Incorrect\n\n```javascript\n// ❌ Non-idempotent POST without protection\napp.post('/payments', async (req, res) => {\n // Each retry creates a duplicate payment!\n const payment = await db.createPayment({\n amount: req.body.amount,\n customerId: req.body.customerId\n });\n await chargeCard(payment);\n res.status(201).json(payment);\n});\n\n// ❌ No idempotency key checking\napp.post('/orders', async (req, res) => {\n // Network timeout after processing = client retries = duplicate order\n const order = await db.createOrder(req.body);\n await processOrder(order);\n res.status(201).json(order);\n});\n```\n\n```json\n// ❌ Client retries without idempotency key\nPOST /payments\n{\n \"amount\": 100,\n \"customerId\": \"cust_123\"\n}\n// Timeout... retry... duplicate payment created!\n```\n\n**Problems:**\n- Duplicate payments or orders when clients retry after network timeouts\n- No way for the server to detect repeated requests\n- Financial losses from double-charging customers\n- Data inconsistency in distributed systems with message retries\n- Clients must implement complex tracking logic to avoid duplicates\n\n## Correct\n\n```javascript\n// ✅ Idempotency key middleware\nconst express = require('express');\nconst router = express.Router();\n\nconst idempotencyStore = new Map(); // Use Redis in production\n\nasync function idempotencyMiddleware(req, res, next) {\n const idempotencyKey = req.headers['idempotency-key'];\n\n if (!idempotencyKey) {\n return res.status(400).json({\n error: 'missing_idempotency_key',\n message: 'Idempotency-Key header is required for this operation'\n });\n }\n\n const cacheKey = `${req.path}:${idempotencyKey}`;\n const cached = idempotencyStore.get(cacheKey);\n\n if (cached) {\n // Return cached response\n return res.status(cached.status).json(cached.body);\n }\n\n // Store original json function\n const originalJson = res.json.bind(res);\n\n // Override to cache response\n res.json = function(body) {\n idempotencyStore.set(cacheKey, {\n status: res.statusCode,\n body: body\n });\n // Set TTL (24 hours typical)\n setTimeout(() => idempotencyStore.delete(cacheKey), 24 * 60 * 60 * 1000);\n return originalJson(body);\n };\n\n next();\n}\n\n// ✅ Apply to non-idempotent operations\nrouter.post('/payments', idempotencyMiddleware, async (req, res) => {\n const payment = await db.createPayment({\n amount: req.body.amount,\n customerId: req.body.customerId,\n idempotencyKey: req.headers['idempotency-key']\n });\n\n await chargeCard(payment);\n res.status(201).json(payment);\n});\n\n// ✅ Idempotent by design using upsert\nrouter.put('/users/:id/preferences', async (req, res) => {\n // PUT is idempotent - same request always produces same result\n const preferences = await db.upsertPreferences(\n req.params.id,\n req.body\n );\n res.json(preferences);\n});\n\n// ✅ Natural idempotency with unique constraints\nrouter.post('/subscriptions', async (req, res) => {\n try {\n const subscription = await db.createSubscription({\n userId: req.body.userId,\n planId: req.body.planId\n });\n res.status(201).json(subscription);\n } catch (error) {\n if (error.code === 'UNIQUE_VIOLATION') {\n // Return existing subscription\n const existing = await db.findSubscription(\n req.body.userId,\n req.body.planId\n );\n return res.status(200).json(existing);\n }\n throw error;\n }\n});\n```\n\n```python\n# ✅ FastAPI with idempotency\nfrom fastapi import FastAPI, Header, HTTPException\nfrom functools import wraps\nimport redis\n\napp = FastAPI()\nredis_client = redis.Redis()\n\ndef idempotent(ttl_seconds: int = 86400):\n def decorator(func):\n @wraps(func)\n async def wrapper(*args, idempotency_key: str = Header(...), **kwargs):\n cache_key = f\"idempotency:{func.__name__}:{idempotency_key}\"\n\n # Check cache\n cached = redis_client.get(cache_key)\n if cached:\n return json.loads(cached)\n\n # Execute operation\n result = await func(*args, **kwargs)\n\n # Cache result\n redis_client.setex(cache_key, ttl_seconds, json.dumps(result))\n\n return result\n return wrapper\n return decorator\n\[email protected](\"/payments\")\n@idempotent(ttl_seconds=86400)\nasync def create_payment(payment: PaymentCreate):\n result = await process_payment(payment)\n return {\"id\": result.id, \"status\": result.status}\n```\n\n```json\n// ✅ Client request with idempotency key\nPOST /payments HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nIdempotency-Key: unique-request-id-12345\n\n{\n \"amount\": 100,\n \"customerId\": \"cust_123\"\n}\n\n// Response (same for retries)\nHTTP/1.1 201 Created\nIdempotency-Key: unique-request-id-12345\n\n{\n \"id\": \"pay_789\",\n \"amount\": 100,\n \"customerId\": \"cust_123\",\n \"status\": \"completed\"\n}\n```\n\n## Idempotency by HTTP Method\n\n| Method | Naturally Idempotent | Notes |\n|--------|---------------------|-------|\n| GET | Yes | Always safe to retry |\n| HEAD | Yes | Always safe to retry |\n| OPTIONS | Yes | Always safe to retry |\n| PUT | Yes | Full replacement is idempotent |\n| DELETE | Yes | Deleting twice = same result |\n| POST | No | Needs idempotency key |\n| PATCH | Usually | Depends on implementation |\n\n**Benefits:**\n- Clients can safely retry requests without causing duplicate operations\n- Prevents duplicate payments or orders that cause financial and data issues\n- Users can safely click \"submit\" multiple times without fear\n- Works well in distributed systems with at-least-once delivery guarantees\n- Idempotency keys provide request correlation across systems\n- Simplifies client code by removing complex success-tracking logic\n\nReference: [Stripe Idempotency Guide](https://stripe.com/docs/api/idempotent_requests)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6253,"content_sha256":"d3e4165f8e672a2b1e5eddb75bf344526a3de42b59b07d3705343d6be0bffa22"},{"filename":"rules/rest-nested-resources.md","content":"---\ntitle: Design Nested Resources for Hierarchical Relationships\nimpact: CRITICAL\nimpactDescription: \"Clarifies resource relationships and authorization boundaries\"\ntags: rest, resources, nesting, hierarchy\n---\n\n## Design Nested Resources for Hierarchical Relationships\n\n**Impact: CRITICAL (Clarifies resource relationships and authorization boundaries)**\n\nUse nested URLs to represent parent-child relationships between resources, but avoid deep nesting beyond two levels.\n\n## Incorrect\n\n```json\n// ❌ Deeply nested resources (3+ levels)\nGET /companies/123/departments/456/employees/789/projects/101/tasks/202\nPOST /organizations/1/teams/2/members/3/assignments/4/subtasks\n\n// ❌ Flat structure losing context\nGET /tasks/202 // Which project? Which employee?\nGET /comments/999 // Comment on what?\n\n// ❌ Inconsistent nesting\nGET /users/123/orders // Nested\nGET /order-items?orderId=456 // Query param\nGET /products/789/reviews // Nested again\n```\n\n```javascript\n// ❌ Overly deep nesting\napp.get('/companies/:companyId/departments/:deptId/employees/:empId/reviews/:reviewId',\n (req, res) => {\n // 4 levels deep - too complex!\n const { companyId, deptId, empId, reviewId } = req.params;\n // ...\n }\n);\n```\n\n**Problems:**\n- URLs become unwieldy and difficult to construct at 3+ levels deep\n- Each nesting level adds required path parameters, complicating client code\n- Inconsistent nesting patterns confuse API consumers\n- Flat structures lose important relationship context\n- Deep nesting makes authorization checks more complex\n\n## Correct\n\n```json\n// ✅ Maximum 2 levels of nesting\nGET /users/123/orders // User's orders\nGET /orders/456/items // Order's items\nGET /posts/789/comments // Post's comments\n\n// Access deep resources directly when needed\nGET /tasks/202 // Direct access with task ID\nGET /employees/789 // Direct access with employee ID\n\n// Use query parameters for filtering\nGET /tasks?projectId=101 // Filter tasks by project\nGET /tasks?employeeId=789&status=active\n```\n\n```javascript\n// ✅ Express router with appropriate nesting\nconst router = express.Router();\n\n// Parent resource\nrouter.get('/users', listUsers);\nrouter.get('/users/:userId', getUser);\nrouter.post('/users', createUser);\n\n// Nested child resource (1 level)\nrouter.get('/users/:userId/orders', getUserOrders);\nrouter.post('/users/:userId/orders', createUserOrder);\nrouter.get('/users/:userId/orders/:orderId', getUserOrder);\n\n// Second-level nested resource (2 levels max)\nrouter.get('/users/:userId/orders/:orderId/items', getOrderItems);\nrouter.post('/users/:userId/orders/:orderId/items', addOrderItem);\n\n// Direct access for deep resources\nrouter.get('/orders/:orderId', getOrder);\nrouter.get('/order-items/:itemId', getOrderItem);\nrouter.patch('/order-items/:itemId', updateOrderItem);\n```\n\n```python\n# ✅ FastAPI with nested resources\nfrom fastapi import APIRouter\n\nrouter = APIRouter()\n\n# Users - parent resource\[email protected](\"/users/{user_id}\")\ndef get_user(user_id: int):\n return db.get_user(user_id)\n\n# Posts - nested under users\[email protected](\"/users/{user_id}/posts\")\ndef get_user_posts(user_id: int, skip: int = 0, limit: int = 10):\n return db.get_posts_by_user(user_id, skip, limit)\n\[email protected](\"/users/{user_id}/posts\")\ndef create_user_post(user_id: int, post: PostCreate):\n return db.create_post(user_id, post)\n\n# Comments - nested under posts (2 levels)\[email protected](\"/posts/{post_id}/comments\")\ndef get_post_comments(post_id: int):\n return db.get_comments_by_post(post_id)\n\n# Direct access for comments when needed\[email protected](\"/comments/{comment_id}\")\ndef get_comment(comment_id: int):\n return db.get_comment(comment_id)\n\[email protected](\"/comments/{comment_id}\")\ndef update_comment(comment_id: int, update: CommentUpdate):\n return db.update_comment(comment_id, update)\n```\n\n```yaml\n# ✅ OpenAPI spec with nested resources\nopenapi: 3.0.0\npaths:\n /users/{userId}/orders:\n get:\n summary: Get all orders for a user\n parameters:\n - name: userId\n in: path\n required: true\n schema:\n type: integer\n\n /users/{userId}/orders/{orderId}:\n get:\n summary: Get a specific order for a user\n\n /orders/{orderId}/items:\n get:\n summary: Get all items in an order\n post:\n summary: Add item to order\n\n # Direct access endpoint\n /orders/{orderId}:\n get:\n summary: Get order by ID directly\n```\n\n**Benefits:**\n- Nested URLs clearly show ownership and hierarchy (e.g., `/users/123/orders`)\n- URL structure makes it easy to enforce authorization boundaries\n- Limiting to 2 levels keeps URLs manageable and predictable\n- Both nested and direct access patterns accommodate different use cases\n- Creating under a parent automatically establishes the relationship\n- Enables specific error messages like \"Order 456 not found for user 123\"\n\nReference: [REST API Design - Resource Relationships](https://restfulapi.net/resource-naming/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5022,"content_sha256":"e3a070f245eb088a1668744696c14408699dbfc6ede40aed075b92a932f36907"},{"filename":"rules/rest-nouns-not-verbs.md","content":"---\ntitle: Use Nouns, Not Verbs for Resource Names\nimpact: CRITICAL\nimpactDescription: \"Foundation of REST architecture\"\ntags: rest, resources, naming, http-methods\n---\n\n## Use Nouns, Not Verbs for Resource Names\n\n**Impact: CRITICAL (Foundation of REST architecture)**\n\nREST API endpoints should represent resources (nouns), not actions (verbs). HTTP methods already convey the action being performed.\n\n## Incorrect\n\n```json\n// ❌ Verbs in endpoint names\nGET /getUsers\nPOST /createUser\nPUT /updateUser/123\nDELETE /deleteUser/123\nGET /fetchAllOrders\nPOST /addNewProduct\n```\n\n```javascript\n// ❌ Express routes with verb-based endpoints\napp.get('/getUsers', getUsers);\napp.post('/createUser', createUser);\napp.get('/fetchUserById/:id', getUserById);\napp.put('/updateUserProfile/:id', updateUser);\napp.delete('/removeUser/:id', deleteUser);\n```\n\n**Problems:**\n- Redundant action verbs when HTTP methods already describe the operation\n- Inconsistent naming across endpoints (get, fetch, create, add)\n- More endpoints than necessary for the same resource\n- URLs become unpredictable and hard to discover\n- Breaks RESTful conventions that developers expect\n- Cannot leverage HTTP method semantics for caching and retry logic\n\n## Correct\n\n```json\n// ✅ Nouns representing resources\nGET /users\nPOST /users\nGET /users/123\nPUT /users/123\nDELETE /users/123\nGET /orders\nPOST /products\n```\n\n```javascript\n// ✅ Express routes with noun-based endpoints\napp.get('/users', listUsers);\napp.post('/users', createUser);\napp.get('/users/:id', getUser);\napp.put('/users/:id', updateUser);\napp.delete('/users/:id', deleteUser);\n```\n\n```python\n# ✅ FastAPI with noun-based resources\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\[email protected](\"/users\")\ndef list_users():\n return users\n\[email protected](\"/users\")\ndef create_user(user: UserCreate):\n return new_user\n\[email protected](\"/users/{user_id}\")\ndef get_user(user_id: int):\n return user\n\[email protected](\"/users/{user_id}\")\ndef update_user(user_id: int, user: UserUpdate):\n return updated_user\n\[email protected](\"/users/{user_id}\")\ndef delete_user(user_id: int):\n return {\"deleted\": True}\n```\n\n**Benefits:**\n- RESTful convention: URLs are resource identifiers, HTTP methods describe actions\n- Consistent and predictable API structure developers can easily understand\n- Fewer endpoints needed since one resource path handles multiple operations\n- Self-documenting resources that map to domain model entities\n- GET requests to noun-based endpoints can be cached effectively\n- Leverages built-in HTTP method semantics\n\nReference: [REST Resource Naming Guide](https://restfulapi.net/resource-naming/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2619,"content_sha256":"13135f78f9e5b620544cec2575e5ffeadbc9305e26d3353eecaf9a43b64930e4"},{"filename":"rules/rest-plural-resources.md","content":"---\ntitle: Use Plural Nouns for Resource Collections\nimpact: CRITICAL\nimpactDescription: \"Improves API consistency and predictability\"\ntags: rest, resources, naming, conventions\n---\n\n## Use Plural Nouns for Resource Collections\n\n**Impact: CRITICAL (Improves API consistency and predictability)**\n\nResource names should consistently use plural nouns to represent collections, maintaining uniformity across your API.\n\n## Incorrect\n\n```json\n// ❌ Inconsistent singular/plural usage\nGET /user // Singular for collection\nGET /user/123 // Singular for individual\nGET /products // Plural for collection\nGET /product/123 // Singular for individual\nGET /order // Inconsistent\nPOST /person // Mixed conventions\n```\n\n```yaml\n# ❌ OpenAPI spec with inconsistent naming\npaths:\n /user:\n get:\n summary: Get all users\n /user/{id}:\n get:\n summary: Get single user\n /products:\n get:\n summary: Get all products\n /product/{id}:\n get:\n summary: Get single product\n```\n\n**Problems:**\n- Developers must guess whether a resource uses singular or plural form\n- Inconsistent patterns across endpoints reduce predictability\n- Ambiguity: `/user` could mean \"current user\" or \"user collection\"\n- Misalignment with database table naming conventions\n- Harder to generate documentation and client SDKs\n\n## Correct\n\n```json\n// ✅ Consistent plural nouns\nGET /users // Collection of users\nGET /users/123 // Single user from collection\nPOST /users // Create user in collection\nPUT /users/123 // Update user in collection\nDELETE /users/123 // Remove user from collection\n\nGET /products // Collection\nGET /products/456 // Single item\nGET /orders // Collection\nGET /orders/789 // Single item\n```\n\n```yaml\n# ✅ OpenAPI spec with consistent plurals\nopenapi: 3.0.0\npaths:\n /users:\n get:\n summary: List all users\n responses:\n '200':\n description: Array of users\n post:\n summary: Create a new user\n\n /users/{userId}:\n get:\n summary: Get a specific user\n put:\n summary: Update a specific user\n delete:\n summary: Delete a specific user\n\n /products:\n get:\n summary: List all products\n\n /products/{productId}:\n get:\n summary: Get a specific product\n```\n\n```javascript\n// ✅ Express router with consistent plurals\nconst router = express.Router();\n\n// Users resource\nrouter.get('/users', listUsers);\nrouter.post('/users', createUser);\nrouter.get('/users/:id', getUser);\nrouter.put('/users/:id', updateUser);\nrouter.delete('/users/:id', deleteUser);\n\n// Products resource\nrouter.get('/products', listProducts);\nrouter.post('/products', createProduct);\nrouter.get('/products/:id', getProduct);\nrouter.put('/products/:id', updateProduct);\nrouter.delete('/products/:id', deleteProduct);\n\n// Orders resource\nrouter.get('/orders', listOrders);\nrouter.post('/orders', createOrder);\nrouter.get('/orders/:id', getOrder);\n```\n\n**Benefits:**\n- Consistent naming eliminates guesswork for developers\n- Predictable patterns: knowing `/users` lets you predict `/products`, `/orders`\n- Plural names clearly indicate collection semantics\n- `/users/123` reads naturally as \"user 123 from the users collection\"\n- Aligns with database table naming conventions (users, products, orders)\n- Matches expectations of REST frameworks and documentation generators\n\nReference: [REST API Tutorial - Resource Naming](https://restfulapi.net/resource-naming/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3476,"content_sha256":"d8354d7c408b010c9b3fae9b66eaeecdfd81419f98ddf2a0833e4aa7381c23fc"},{"filename":"rules/rest-resource-actions.md","content":"---\ntitle: Handle Non-CRUD Actions on Resources\nimpact: CRITICAL\nimpactDescription: \"Proper handling of complex operations and state transitions\"\ntags: rest, actions, state-transitions, workflows\n---\n\n## Handle Non-CRUD Actions on Resources\n\n**Impact: CRITICAL (Proper handling of complex operations and state transitions)**\n\nSome operations don't fit standard CRUD patterns. Use sub-resources or action endpoints for operations that represent state transitions or complex actions.\n\n## Incorrect\n\n```json\n// ❌ Verbs in main resource path\nPOST /activateUser/123\nPOST /deactivateUser/123\nPOST /sendEmailToUser/123\nPOST /approveOrder/456\nPOST /shipOrder/456\n\n// ❌ Query parameters for actions\nPOST /users/123?action=activate\nPOST /orders/456?action=ship&tracking=ABC123\n```\n\n```javascript\n// ❌ Complex PATCH with action semantics\napp.patch('/orders/:id', (req, res) => {\n // Trying to use PATCH for everything\n if (req.body.status === 'shipped') {\n // Shipping logic, tracking number, notifications...\n } else if (req.body.status === 'cancelled') {\n // Cancellation logic, refund, restock...\n }\n});\n```\n\n**Problems:**\n- Verb-based paths break RESTful naming conventions\n- Query parameter actions are unpredictable and hard to document\n- Overloaded PATCH endpoints hide complex business logic and side effects\n- Difficult to apply fine-grained authorization per action\n- State transition validation gets tangled in a single handler\n- No clear audit trail of specific operations performed\n\n## Correct\n\n```javascript\n// ✅ Sub-resource actions (noun-based)\nconst router = express.Router();\n\n// User lifecycle actions\nrouter.post('/users/:id/activation', activateUser); // Activate user\nrouter.delete('/users/:id/activation', deactivateUser); // Deactivate user\nrouter.post('/users/:id/password-reset', resetPassword); // Reset password\nrouter.post('/users/:id/verification', sendVerification); // Send verification\n\n// Order workflow actions\nrouter.post('/orders/:id/shipment', shipOrder); // Ship order\nrouter.post('/orders/:id/cancellation', cancelOrder); // Cancel order\nrouter.post('/orders/:id/refund', refundOrder); // Refund order\n\n// Controller actions as verbs when necessary\nrouter.post('/orders/:id/ship', async (req, res) => {\n const { trackingNumber, carrier } = req.body;\n const order = await orderService.ship(req.params.id, {\n trackingNumber,\n carrier\n });\n res.json(order);\n});\n\nrouter.post('/orders/:id/cancel', async (req, res) => {\n const { reason } = req.body;\n const order = await orderService.cancel(req.params.id, reason);\n res.json(order);\n});\n```\n\n```python\n# ✅ FastAPI with action endpoints\nfrom fastapi import APIRouter, HTTPException\nfrom enum import Enum\n\nrouter = APIRouter()\n\nclass OrderStatus(str, Enum):\n PENDING = \"pending\"\n CONFIRMED = \"confirmed\"\n SHIPPED = \"shipped\"\n DELIVERED = \"delivered\"\n CANCELLED = \"cancelled\"\n\n# State transition actions\[email protected](\"/orders/{order_id}/confirm\")\nasync def confirm_order(order_id: int):\n order = await db.get_order(order_id)\n if order.status != OrderStatus.PENDING:\n raise HTTPException(\n status_code=422,\n detail=f\"Cannot confirm order in {order.status} status\"\n )\n order.status = OrderStatus.CONFIRMED\n await db.save_order(order)\n await notification_service.send_confirmation(order)\n return order\n\[email protected](\"/orders/{order_id}/ship\")\nasync def ship_order(order_id: int, shipment: ShipmentInfo):\n order = await db.get_order(order_id)\n if order.status != OrderStatus.CONFIRMED:\n raise HTTPException(\n status_code=422,\n detail=f\"Cannot ship order in {order.status} status\"\n )\n\n order.status = OrderStatus.SHIPPED\n order.tracking_number = shipment.tracking_number\n order.carrier = shipment.carrier\n order.shipped_at = datetime.utcnow()\n\n await db.save_order(order)\n await notification_service.send_shipping_notification(order)\n return order\n\[email protected](\"/orders/{order_id}/cancel\")\nasync def cancel_order(order_id: int, cancellation: CancellationRequest):\n order = await db.get_order(order_id)\n if order.status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED]:\n raise HTTPException(\n status_code=422,\n detail=\"Cannot cancel shipped or delivered orders\"\n )\n\n order.status = OrderStatus.CANCELLED\n order.cancellation_reason = cancellation.reason\n\n await db.save_order(order)\n await payment_service.refund(order)\n await inventory_service.restock(order.items)\n\n return order\n\n# Batch actions\[email protected](\"/orders/bulk-ship\")\nasync def bulk_ship_orders(request: BulkShipRequest):\n results = []\n for order_id in request.order_ids:\n try:\n result = await ship_order(order_id, request.shipment_info)\n results.append({\"order_id\": order_id, \"status\": \"shipped\"})\n except HTTPException as e:\n results.append({\"order_id\": order_id, \"status\": \"failed\", \"error\": e.detail})\n return {\"results\": results}\n```\n\n```yaml\n# ✅ OpenAPI spec for actions\nopenapi: 3.0.0\npaths:\n /orders/{orderId}/ship:\n post:\n summary: Ship an order\n description: Transitions order to shipped status\n parameters:\n - name: orderId\n in: path\n required: true\n schema:\n type: integer\n requestBody:\n required: true\n content:\n application/json:\n schema:\n type: object\n required:\n - trackingNumber\n - carrier\n properties:\n trackingNumber:\n type: string\n carrier:\n type: string\n enum: [ups, fedex, usps, dhl]\n responses:\n '200':\n description: Order shipped successfully\n '422':\n description: Order cannot be shipped in current state\n\n /orders/{orderId}/cancel:\n post:\n summary: Cancel an order\n requestBody:\n content:\n application/json:\n schema:\n type: object\n properties:\n reason:\n type: string\n responses:\n '200':\n description: Order cancelled successfully\n '422':\n description: Order cannot be cancelled\n```\n\n## Patterns for Actions\n\n| Action Type | Pattern | Example |\n|-------------|---------|---------|\n| State transition | POST /resource/{id}/action | POST /orders/123/ship |\n| Sub-resource creation | POST /resource/{id}/sub-resource | POST /users/123/password-reset |\n| Batch operation | POST /resources/bulk-action | POST /orders/bulk-ship |\n| Controller action | POST /action (no resource ID) | POST /search, POST /calculate |\n\n**Benefits:**\n- Action endpoints explicitly show what operation is being performed\n- Each action can have specific validation rules and business logic\n- Complex state transitions are better modeled as explicit actions than PATCH\n- Side effects (notifications, payments) get dedicated endpoints\n- Fine-grained authorization (can ship orders vs. can cancel orders)\n- Each action creates a clear, auditable record of what happened\n\nReference: [REST API Design - Actions](https://restfulapi.net/rest-api-design-tutorial-with-example/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7332,"content_sha256":"0f5d05c7e5b151d4dda14757204562de482a1fcf9bc72498c5af04aae125bb77"},{"filename":"rules/rest-status-codes.md","content":"---\ntitle: Use Appropriate HTTP Status Codes\nimpact: CRITICAL\nimpactDescription: \"Enables proper client handling, caching, and monitoring\"\ntags: rest, http-status, errors, semantics\n---\n\n## Use Appropriate HTTP Status Codes\n\n**Impact: CRITICAL (Enables proper client handling, caching, and monitoring)**\n\nReturn semantically correct HTTP status codes that accurately describe the result of the operation.\n\n## Incorrect\n\n```javascript\n// ❌ Always returning 200\napp.post('/users', async (req, res) => {\n try {\n const user = await db.createUser(req.body);\n res.status(200).json(user); // Should be 201 Created\n } catch (error) {\n res.status(200).json({ error: error.message }); // Error with 200!\n }\n});\n\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n if (!user) {\n res.status(200).json({ error: 'Not found' }); // Should be 404\n }\n res.status(200).json(user);\n});\n\napp.delete('/users/:id', async (req, res) => {\n await db.deleteUser(req.params.id);\n res.status(200).json({ message: 'Deleted' }); // 204 is more appropriate\n});\n```\n\n```json\n// ❌ Error responses with 200 status\nHTTP/1.1 200 OK\n{\n \"success\": false,\n \"error\": \"User not found\"\n}\n```\n\n**Problems:**\n- Clients cannot distinguish success from failure without parsing the body\n- HTTP caches will cache error responses as successful\n- Monitoring and APM tools cannot track real error rates\n- Retry logic cannot determine whether to retry based on status code\n- Breaks HTTP standards and confuses developers\n\n## Correct\n\n```javascript\nconst express = require('express');\nconst router = express.Router();\n\n// ✅ 200 OK - Successful GET, PUT, PATCH\nrouter.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n if (!user) {\n return res.status(404).json({\n error: 'not_found',\n message: 'User not found'\n });\n }\n res.status(200).json(user);\n});\n\n// ✅ 201 Created - Successful POST that creates a resource\nrouter.post('/users', async (req, res) => {\n const user = await db.createUser(req.body);\n res.status(201)\n .location(`/users/${user.id}`)\n .json(user);\n});\n\n// ✅ 204 No Content - Successful DELETE or update with no response body\nrouter.delete('/users/:id', async (req, res) => {\n const deleted = await db.deleteUser(req.params.id);\n if (!deleted) {\n return res.status(404).json({\n error: 'not_found',\n message: 'User not found'\n });\n }\n res.status(204).send();\n});\n\n// ✅ 400 Bad Request - Invalid input\nrouter.post('/users', async (req, res) => {\n if (!req.body.email) {\n return res.status(400).json({\n error: 'validation_error',\n message: 'Email is required',\n field: 'email'\n });\n }\n // ...\n});\n\n// ✅ 401 Unauthorized - Not authenticated\nrouter.use((req, res, next) => {\n if (!req.headers.authorization) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Authentication required'\n });\n }\n next();\n});\n\n// ✅ 403 Forbidden - Authenticated but not authorized\nrouter.delete('/users/:id', async (req, res) => {\n if (req.user.id !== req.params.id && !req.user.isAdmin) {\n return res.status(403).json({\n error: 'forbidden',\n message: 'You cannot delete other users'\n });\n }\n // ...\n});\n\n// ✅ 409 Conflict - Resource conflict\nrouter.post('/users', async (req, res) => {\n const exists = await db.userExists(req.body.email);\n if (exists) {\n return res.status(409).json({\n error: 'conflict',\n message: 'User with this email already exists'\n });\n }\n // ...\n});\n\n// ✅ 422 Unprocessable Entity - Semantic validation error\nrouter.post('/orders', async (req, res) => {\n const product = await db.findProduct(req.body.productId);\n if (product.stock \u003c req.body.quantity) {\n return res.status(422).json({\n error: 'unprocessable_entity',\n message: 'Insufficient stock',\n available: product.stock\n });\n }\n // ...\n});\n```\n\n## Common Status Codes Reference\n\n### Success (2xx)\n| Code | Name | Use Case |\n|------|------|----------|\n| 200 | OK | Successful GET, PUT, PATCH |\n| 201 | Created | Successful POST creating resource |\n| 202 | Accepted | Request accepted for async processing |\n| 204 | No Content | Successful DELETE or update with no body |\n\n### Client Errors (4xx)\n| Code | Name | Use Case |\n|------|------|----------|\n| 400 | Bad Request | Malformed syntax, invalid JSON |\n| 401 | Unauthorized | Missing or invalid authentication |\n| 403 | Forbidden | Authenticated but not authorized |\n| 404 | Not Found | Resource doesn't exist |\n| 405 | Method Not Allowed | HTTP method not supported |\n| 409 | Conflict | Resource conflict (duplicate) |\n| 422 | Unprocessable Entity | Validation/business logic error |\n| 429 | Too Many Requests | Rate limit exceeded |\n\n### Server Errors (5xx)\n| Code | Name | Use Case |\n|------|------|----------|\n| 500 | Internal Server Error | Unexpected server error |\n| 502 | Bad Gateway | Upstream service error |\n| 503 | Service Unavailable | Server temporarily unavailable |\n| 504 | Gateway Timeout | Upstream service timeout |\n\n**Benefits:**\n- Status codes convey meaning before clients parse the response body\n- HTTP clients, browsers, and tools handle different status codes appropriately\n- Correct codes enable proper caching (2xx cached, 4xx/5xx not)\n- Infrastructure and APM tools use status codes to track error rates and API health\n- Clients can implement smart retry logic (retry 503, don't retry 400)\n- Following HTTP standards ensures interoperability with tools and services\n\nReference: [MDN HTTP Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5648,"content_sha256":"ffb61e125378fbc101f1e8fa58a75f7974370892c765f286bcde6494aec1e2e2"},{"filename":"rules/sec-authentication.md","content":"---\ntitle: Implement Secure Authentication\nimpact: CRITICAL\nimpactDescription: \"Protects user data and prevents unauthorized access\"\ntags: security, authentication, jwt, oauth2, tokens\n---\n\n## Implement Secure Authentication\n\n**Impact: CRITICAL (Protects user data and prevents unauthorized access)**\n\nUse industry-standard authentication mechanisms like OAuth 2.0, JWT, or API keys with proper security practices.\n\n## Incorrect\n\n```javascript\n// ❌ Basic auth over HTTP\napp.use((req, res, next) => {\n const auth = req.headers.authorization;\n const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64')\n .toString().split(':');\n // Credentials sent in plain text!\n if (user === 'admin' && pass === 'password123') {\n next();\n }\n});\n\n// ❌ Token in URL\napp.get('/users?token=secret123', (req, res) => {\n // Token visible in logs, browser history, referrer headers\n});\n\n// ❌ No token expiration\nconst token = jwt.sign({ userId: 123 }); // No expiration!\n\n// ❌ Weak secret\nconst token = jwt.sign({ userId: 123 }, 'secret'); // Easily guessable\n```\n\n```json\n// ❌ Credentials in response body\n{\n \"user\": {\n \"id\": 123,\n \"password\": \"hashedpassword\",\n \"apiKey\": \"sk_live_abc123\"\n }\n}\n```\n\n**Problems:**\n- Plain text credentials can be intercepted over HTTP\n- Tokens in URLs are logged by servers, proxies, and browsers\n- Non-expiring tokens remain valid forever if compromised\n- Weak secrets allow token forgery through brute force\n- Exposing credentials in responses enables account takeover\n\n## Correct\n\n```javascript\n// ✅ Secure JWT authentication\nconst jwt = require('jsonwebtoken');\nconst bcrypt = require('bcrypt');\n\n// Environment-based secrets\nconst JWT_SECRET = process.env.JWT_SECRET; // Long, random string\nconst JWT_EXPIRES_IN = '15m'; // Short-lived access tokens\nconst REFRESH_EXPIRES_IN = '7d';\n\n// Password hashing\nasync function hashPassword(password) {\n const saltRounds = 12;\n return bcrypt.hash(password, saltRounds);\n}\n\nasync function verifyPassword(password, hash) {\n return bcrypt.compare(password, hash);\n}\n\n// JWT token generation\nfunction generateTokens(user) {\n const accessToken = jwt.sign(\n {\n sub: user.id,\n email: user.email,\n roles: user.roles\n },\n JWT_SECRET,\n {\n expiresIn: JWT_EXPIRES_IN,\n issuer: 'api.example.com',\n audience: 'example.com'\n }\n );\n\n const refreshToken = jwt.sign(\n { sub: user.id, type: 'refresh' },\n JWT_SECRET,\n { expiresIn: REFRESH_EXPIRES_IN }\n );\n\n return { accessToken, refreshToken };\n}\n\n// Authentication endpoint\napp.post('/auth/login', async (req, res) => {\n const { email, password } = req.body;\n\n const user = await db.findUserByEmail(email);\n if (!user) {\n // Don't reveal if user exists\n return res.status(401).json({\n error: {\n code: 'invalid_credentials',\n message: 'Invalid email or password'\n }\n });\n }\n\n const valid = await verifyPassword(password, user.passwordHash);\n if (!valid) {\n return res.status(401).json({\n error: {\n code: 'invalid_credentials',\n message: 'Invalid email or password'\n }\n });\n }\n\n const tokens = generateTokens(user);\n\n // Set refresh token as HTTP-only cookie\n res.cookie('refreshToken', tokens.refreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: 'strict',\n maxAge: 7 * 24 * 60 * 60 * 1000\n });\n\n res.json({\n accessToken: tokens.accessToken,\n expiresIn: 900, // 15 minutes in seconds\n tokenType: 'Bearer'\n });\n});\n\n// JWT verification middleware\nfunction authenticate(req, res, next) {\n const authHeader = req.headers.authorization;\n\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n return res.status(401).json({\n error: {\n code: 'auth_token_missing',\n message: 'Authorization header with Bearer token required'\n }\n });\n }\n\n const token = authHeader.slice(7);\n\n try {\n const payload = jwt.verify(token, JWT_SECRET, {\n issuer: 'api.example.com',\n audience: 'example.com'\n });\n\n req.user = payload;\n next();\n } catch (error) {\n if (error.name === 'TokenExpiredError') {\n return res.status(401).json({\n error: {\n code: 'auth_token_expired',\n message: 'Access token has expired. Please refresh.'\n }\n });\n }\n\n return res.status(401).json({\n error: {\n code: 'auth_token_invalid',\n message: 'Invalid access token'\n }\n });\n }\n}\n\n// Token refresh endpoint\napp.post('/auth/refresh', async (req, res) => {\n const refreshToken = req.cookies.refreshToken;\n\n if (!refreshToken) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_missing',\n message: 'Refresh token required'\n }\n });\n }\n\n try {\n const payload = jwt.verify(refreshToken, JWT_SECRET);\n\n // Check if token is revoked\n const isRevoked = await db.isTokenRevoked(refreshToken);\n if (isRevoked) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_revoked',\n message: 'Refresh token has been revoked'\n }\n });\n }\n\n const user = await db.findUser(payload.sub);\n const tokens = generateTokens(user);\n\n // Rotate refresh token\n await db.revokeToken(refreshToken);\n\n res.cookie('refreshToken', tokens.refreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: 'strict',\n maxAge: 7 * 24 * 60 * 60 * 1000\n });\n\n res.json({\n accessToken: tokens.accessToken,\n expiresIn: 900\n });\n } catch (error) {\n return res.status(401).json({\n error: {\n code: 'refresh_token_invalid',\n message: 'Invalid refresh token'\n }\n });\n }\n});\n\n// Protected route\napp.get('/users/me', authenticate, (req, res) => {\n res.json({ userId: req.user.sub });\n});\n```\n\n```python\n# ✅ FastAPI with OAuth2 and JWT\nfrom fastapi import FastAPI, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm\nfrom jose import JWTError, jwt\nfrom passlib.context import CryptContext\nfrom datetime import datetime, timedelta\nimport os\n\napp = FastAPI()\n\nSECRET_KEY = os.getenv(\"JWT_SECRET\")\nALGORITHM = \"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES = 15\n\npwd_context = CryptContext(schemes=[\"bcrypt\"], deprecated=\"auto\")\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"auth/token\")\n\ndef create_access_token(data: dict, expires_delta: timedelta = None):\n to_encode = data.copy()\n expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))\n to_encode.update({\"exp\": expire, \"iss\": \"api.example.com\"})\n return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n\nasync def get_current_user(token: str = Depends(oauth2_scheme)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail={\"code\": \"auth_token_invalid\", \"message\": \"Invalid token\"},\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n user_id: str = payload.get(\"sub\")\n if user_id is None:\n raise credentials_exception\n except JWTError:\n raise credentials_exception\n\n user = await db.get_user(user_id)\n if user is None:\n raise credentials_exception\n return user\n\[email protected](\"/auth/token\")\nasync def login(form_data: OAuth2PasswordRequestForm = Depends()):\n user = await authenticate_user(form_data.username, form_data.password)\n if not user:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail={\"code\": \"invalid_credentials\", \"message\": \"Invalid credentials\"}\n )\n\n access_token = create_access_token(\n data={\"sub\": str(user.id)},\n expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\[email protected](\"/users/me\")\nasync def read_users_me(current_user = Depends(get_current_user)):\n return current_user\n```\n\n**Benefits:**\n- Short-lived tokens and refresh rotation limit damage from token theft\n- Bcrypt hashing protects passwords even if the database is compromised\n- HTTP-only cookies prevent XSS attacks on refresh tokens\n- OAuth 2.0/JWT are well-understood, widely supported standards\n- Stateless JWT tokens work well in distributed systems\n- Token-based auth provides clear user identification for audit logging\n\nReference: [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8540,"content_sha256":"aac7ff9d860b8ba96522f6f15d864e7050d5690873517c165dd315fea309f880"},{"filename":"rules/sec-authorization.md","content":"---\ntitle: Implement Proper Authorization\nimpact: CRITICAL\nimpactDescription: \"Enforces access control and resource permissions\"\ntags: security, authorization, rbac, permissions, access-control\n---\n\n## Implement Proper Authorization\n\n**Impact: CRITICAL (Enforces access control and resource permissions)**\n\nAuthorization verifies what authenticated users can do. Implement role-based (RBAC) or attribute-based (ABAC) access control consistently.\n\n## Incorrect\n\n```javascript\n// ❌ No authorization checks\napp.delete('/users/:id', authenticate, async (req, res) => {\n // Anyone authenticated can delete any user!\n await db.deleteUser(req.params.id);\n res.status(204).send();\n});\n\n// ❌ Client-side only authorization\napp.get('/admin/users', authenticate, async (req, res) => {\n // Relies on frontend hiding the button\n const users = await db.getAllUsers();\n res.json(users);\n});\n\n// ❌ Inconsistent checks\napp.get('/documents/:id', async (req, res) => {\n const doc = await db.findDocument(req.params.id);\n // Sometimes checks, sometimes doesn't\n if (doc.isPublic) {\n res.json(doc);\n }\n // Private docs accessible without check!\n res.json(doc);\n});\n```\n\n**Problems:**\n- Any authenticated user can perform any operation on any resource\n- Client-side authorization can be bypassed by calling the API directly\n- Inconsistent checks leave gaps attackers can exploit\n- No separation between authentication (who) and authorization (what)\n- No audit trail of permission-based access decisions\n\n## Correct\n\n```javascript\n// ✅ Role-Based Access Control (RBAC)\nconst ROLES = {\n ADMIN: 'admin',\n MANAGER: 'manager',\n USER: 'user'\n};\n\nconst PERMISSIONS = {\n // Resource: action -> roles that can perform it\n users: {\n read: [ROLES.ADMIN, ROLES.MANAGER, ROLES.USER],\n create: [ROLES.ADMIN, ROLES.MANAGER],\n update: [ROLES.ADMIN, ROLES.MANAGER],\n delete: [ROLES.ADMIN]\n },\n reports: {\n read: [ROLES.ADMIN, ROLES.MANAGER],\n create: [ROLES.ADMIN, ROLES.MANAGER],\n delete: [ROLES.ADMIN]\n },\n settings: {\n read: [ROLES.ADMIN],\n update: [ROLES.ADMIN]\n }\n};\n\n// Authorization middleware\nfunction authorize(resource, action) {\n return (req, res, next) => {\n const userRoles = req.user.roles || [];\n const allowedRoles = PERMISSIONS[resource]?.[action] || [];\n\n const hasPermission = userRoles.some(role =>\n allowedRoles.includes(role)\n );\n\n if (!hasPermission) {\n return res.status(403).json({\n error: {\n code: 'forbidden',\n message: `You don't have permission to ${action} ${resource}`,\n requiredRoles: allowedRoles,\n yourRoles: userRoles\n }\n });\n }\n\n next();\n };\n}\n\n// Resource ownership check\nasync function authorizeOwnership(req, res, next) {\n const resourceId = req.params.id;\n const userId = req.user.sub;\n\n const resource = await db.findResource(resourceId);\n\n if (!resource) {\n return res.status(404).json({\n error: { code: 'not_found', message: 'Resource not found' }\n });\n }\n\n // Admin can access anything\n if (req.user.roles.includes(ROLES.ADMIN)) {\n req.resource = resource;\n return next();\n }\n\n // Owner can access their own resources\n if (resource.ownerId !== userId) {\n return res.status(403).json({\n error: {\n code: 'forbidden',\n message: 'You can only access your own resources'\n }\n });\n }\n\n req.resource = resource;\n next();\n}\n\n// Usage\napp.get('/users',\n authenticate,\n authorize('users', 'read'),\n async (req, res) => {\n const users = await db.getUsers();\n res.json(users);\n }\n);\n\napp.delete('/users/:id',\n authenticate,\n authorize('users', 'delete'),\n async (req, res) => {\n await db.deleteUser(req.params.id);\n res.status(204).send();\n }\n);\n\napp.get('/documents/:id',\n authenticate,\n authorizeOwnership,\n async (req, res) => {\n res.json(req.resource);\n }\n);\n\napp.put('/documents/:id',\n authenticate,\n authorizeOwnership,\n async (req, res) => {\n const updated = await db.updateDocument(req.params.id, req.body);\n res.json(updated);\n }\n);\n```\n\n```python\n# ✅ FastAPI with RBAC\nfrom fastapi import FastAPI, Depends, HTTPException, status\nfrom enum import Enum\nfrom typing import List\nfrom functools import wraps\n\napp = FastAPI()\n\nclass Role(str, Enum):\n ADMIN = \"admin\"\n MANAGER = \"manager\"\n USER = \"user\"\n\nclass Permission(str, Enum):\n READ_USERS = \"read:users\"\n WRITE_USERS = \"write:users\"\n DELETE_USERS = \"delete:users\"\n READ_REPORTS = \"read:reports\"\n ADMIN_SETTINGS = \"admin:settings\"\n\nROLE_PERMISSIONS = {\n Role.ADMIN: [\n Permission.READ_USERS,\n Permission.WRITE_USERS,\n Permission.DELETE_USERS,\n Permission.READ_REPORTS,\n Permission.ADMIN_SETTINGS\n ],\n Role.MANAGER: [\n Permission.READ_USERS,\n Permission.WRITE_USERS,\n Permission.READ_REPORTS\n ],\n Role.USER: [\n Permission.READ_USERS\n ]\n}\n\ndef get_user_permissions(user) -> List[Permission]:\n permissions = set()\n for role in user.roles:\n permissions.update(ROLE_PERMISSIONS.get(role, []))\n return list(permissions)\n\ndef require_permission(permission: Permission):\n def decorator(func):\n @wraps(func)\n async def wrapper(*args, current_user = Depends(get_current_user), **kwargs):\n user_permissions = get_user_permissions(current_user)\n\n if permission not in user_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail={\n \"code\": \"forbidden\",\n \"message\": f\"Permission '{permission.value}' required\",\n \"your_permissions\": [p.value for p in user_permissions]\n }\n )\n return await func(*args, current_user=current_user, **kwargs)\n return wrapper\n return decorator\n\ndef require_ownership_or_admin(resource_type: str):\n async def check_ownership(\n resource_id: int,\n current_user = Depends(get_current_user)\n ):\n resource = await db.get_resource(resource_type, resource_id)\n\n if not resource:\n raise HTTPException(status_code=404, detail=\"Not found\")\n\n if Role.ADMIN in current_user.roles:\n return resource\n\n if resource.owner_id != current_user.id:\n raise HTTPException(\n status_code=403,\n detail={\n \"code\": \"forbidden\",\n \"message\": \"You can only access your own resources\"\n }\n )\n return resource\n return check_ownership\n\[email protected](\"/users\")\n@require_permission(Permission.READ_USERS)\nasync def list_users(current_user = Depends(get_current_user)):\n return await db.get_users()\n\[email protected](\"/users/{user_id}\")\n@require_permission(Permission.DELETE_USERS)\nasync def delete_user(user_id: int, current_user = Depends(get_current_user)):\n await db.delete_user(user_id)\n return {\"deleted\": True}\n\[email protected](\"/documents/{document_id}\")\nasync def get_document(\n document = Depends(require_ownership_or_admin(\"documents\"))\n):\n return document\n```\n\n```json\n// ✅ Authorization error response\n{\n \"error\": {\n \"code\": \"forbidden\",\n \"message\": \"You don't have permission to delete users\",\n \"details\": {\n \"requiredPermission\": \"delete:users\",\n \"yourRoles\": [\"user\", \"manager\"],\n \"requiredRoles\": [\"admin\"]\n }\n }\n}\n```\n\n## Authorization Patterns\n\n| Pattern | Use Case | Example |\n|---------|----------|---------|\n| RBAC | Role-based access | Admin, Manager, User roles |\n| ABAC | Attribute-based | Department, location, time-based |\n| Ownership | Resource owners | User owns their documents |\n| Hierarchical | Org structure | Managers see team's data |\n| Feature flags | Feature access | Premium features |\n\n**Benefits:**\n- Prevents unauthorized access to sensitive data and operations\n- Principle of least privilege: users only get access they need\n- Centralized permission definitions are easy to audit and update\n- Server-side checks cannot be bypassed like client-side authorization\n- RBAC/ABAC scales better than per-user permissions\n- Clear authorization rules support compliance requirements\n\nReference: [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8393,"content_sha256":"8f817c2e40fbb4b60429b521c193fa3b13c30a102a87c01009798678f33e03c8"},{"filename":"rules/sec-cors-config.md","content":"---\ntitle: Configure CORS Properly\nimpact: CRITICAL\nimpactDescription: \"Prevents unauthorized cross-origin access\"\ntags: security, cors, cross-origin, browsers\n---\n\n## Configure CORS Properly\n\n**Impact: CRITICAL (Prevents unauthorized cross-origin access)**\n\nCross-Origin Resource Sharing (CORS) must be configured correctly to allow legitimate cross-origin requests while preventing unauthorized access.\n\n## Incorrect\n\n```javascript\n// ❌ Allow all origins\napp.use(cors({\n origin: '*',\n credentials: true // DANGEROUS: Can't use * with credentials!\n}));\n\n// ❌ Reflecting origin without validation\napp.use((req, res, next) => {\n // Allows ANY origin - security vulnerability\n res.header('Access-Control-Allow-Origin', req.headers.origin);\n res.header('Access-Control-Allow-Credentials', 'true');\n next();\n});\n\n// ❌ Overly permissive headers\napp.use(cors({\n origin: '*',\n methods: '*',\n allowedHeaders: '*',\n exposedHeaders: '*'\n}));\n\n// ❌ Missing CORS entirely for API\napp.get('/api/data', (req, res) => {\n // Browser will block cross-origin requests\n res.json({ data: 'value' });\n});\n```\n\n**Problems:**\n- Wildcard origin with credentials allows any website to make authenticated requests\n- Reflecting the Origin header blindly defeats the purpose of CORS\n- Overly permissive headers expose the API to cross-origin attacks\n- Missing CORS configuration blocks legitimate frontend applications\n- Enables CSRF attacks that rely on cross-origin requests\n\n## Correct\n\n```javascript\n// ✅ Properly configured CORS\nconst cors = require('cors');\n\n// Allowed origins whitelist\nconst allowedOrigins = [\n 'https://myapp.com',\n 'https://www.myapp.com',\n 'https://admin.myapp.com'\n];\n\n// Development origins (only in non-production)\nif (process.env.NODE_ENV !== 'production') {\n allowedOrigins.push(\n 'http://localhost:3000',\n 'http://localhost:8080'\n );\n}\n\n// CORS configuration\nconst corsOptions = {\n origin: (origin, callback) => {\n // Allow requests with no origin (mobile apps, curl, etc.)\n if (!origin) {\n return callback(null, true);\n }\n\n if (allowedOrigins.includes(origin)) {\n callback(null, true);\n } else {\n callback(new Error('Not allowed by CORS'));\n }\n },\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n allowedHeaders: [\n 'Content-Type',\n 'Authorization',\n 'X-Request-ID',\n 'X-Requested-With'\n ],\n exposedHeaders: [\n 'X-Request-ID',\n 'X-RateLimit-Limit',\n 'X-RateLimit-Remaining',\n 'X-RateLimit-Reset'\n ],\n credentials: true,\n maxAge: 86400, // 24 hours - cache preflight requests\n optionsSuccessStatus: 204\n};\n\napp.use(cors(corsOptions));\n\n// Handle CORS errors\napp.use((err, req, res, next) => {\n if (err.message === 'Not allowed by CORS') {\n return res.status(403).json({\n error: {\n code: 'cors_error',\n message: 'Cross-origin request blocked',\n origin: req.headers.origin\n }\n });\n }\n next(err);\n});\n\n// Route-specific CORS (for public endpoints)\nconst publicCors = cors({\n origin: '*',\n methods: ['GET'],\n maxAge: 86400\n});\n\napp.get('/api/public/health', publicCors, (req, res) => {\n res.json({ status: 'ok' });\n});\n\n// Strict CORS for sensitive endpoints\nconst strictCors = cors({\n origin: 'https://admin.myapp.com',\n credentials: true,\n methods: ['GET', 'POST', 'DELETE']\n});\n\napp.use('/api/admin', strictCors, adminRouter);\n```\n\n```python\n# ✅ FastAPI CORS configuration\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nimport os\n\napp = FastAPI()\n\n# Define allowed origins\nallowed_origins = [\n \"https://myapp.com\",\n \"https://www.myapp.com\",\n \"https://admin.myapp.com\"\n]\n\n# Add development origins\nif os.getenv(\"ENVIRONMENT\") != \"production\":\n allowed_origins.extend([\n \"http://localhost:3000\",\n \"http://localhost:8080\"\n ])\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=allowed_origins,\n allow_credentials=True,\n allow_methods=[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n allow_headers=[\n \"Content-Type\",\n \"Authorization\",\n \"X-Request-ID\",\n \"X-Requested-With\"\n ],\n expose_headers=[\n \"X-Request-ID\",\n \"X-RateLimit-Limit\",\n \"X-RateLimit-Remaining\"\n ],\n max_age=86400\n)\n\n# For public endpoints, create a sub-application\npublic_app = FastAPI()\npublic_app.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Public API\n allow_methods=[\"GET\"],\n max_age=86400\n)\n\napp.mount(\"/public\", public_app)\n```\n\n```typescript\n// ✅ Dynamic CORS based on subdomain pattern\nconst corsOptions = {\n origin: (origin: string | undefined, callback: Function) => {\n if (!origin) {\n return callback(null, true);\n }\n\n // Allow all subdomains of myapp.com\n const allowedPattern = /^https:\\/\\/([a-z0-9-]+\\.)?myapp\\.com$/;\n\n if (allowedPattern.test(origin)) {\n callback(null, true);\n } else {\n callback(new Error('CORS not allowed'));\n }\n },\n credentials: true\n};\n```\n\n## CORS Headers Reference\n\n| Header | Purpose | Example |\n|--------|---------|---------|\n| Access-Control-Allow-Origin | Allowed origins | `https://myapp.com` |\n| Access-Control-Allow-Methods | Allowed HTTP methods | `GET, POST, PUT` |\n| Access-Control-Allow-Headers | Allowed request headers | `Content-Type, Authorization` |\n| Access-Control-Expose-Headers | Headers client can read | `X-Request-ID` |\n| Access-Control-Allow-Credentials | Allow cookies/auth | `true` |\n| Access-Control-Max-Age | Preflight cache time | `86400` |\n\n## Common Patterns\n\n```javascript\n// ✅ Pattern 1: Subdomain allowlist\nconst isAllowedOrigin = (origin) => {\n if (!origin) return true;\n const url = new URL(origin);\n return url.hostname.endsWith('.myapp.com');\n};\n\n// ✅ Pattern 2: Environment-based\nconst origins = {\n production: ['https://myapp.com'],\n staging: ['https://staging.myapp.com'],\n development: ['http://localhost:3000']\n};\nconst allowedOrigins = origins[process.env.NODE_ENV];\n\n// ✅ Pattern 3: Database-driven (for multi-tenant)\nconst corsOptions = {\n origin: async (origin, callback) => {\n const tenant = await db.findTenantByDomain(origin);\n callback(null, tenant?.corsEnabled ?? false);\n }\n};\n```\n\n**Benefits:**\n- Prevents malicious websites from making unauthorized API calls\n- Proper configuration prevents credential leakage to untrusted origins\n- Blocks CSRF attacks that rely on cross-origin requests\n- Explicitly controls which domains can access your API\n- Preflight caching reduces OPTIONS request overhead\n- Route-specific CORS allows different policies for different endpoints\n\nReference: [MDN CORS Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6707,"content_sha256":"ee3a4cca21ab7168731565a46ae6e7088ceb9587af3e8da2cb05bc7ce91ed0e1"},{"filename":"rules/sec-https-only.md","content":"---\ntitle: Enforce HTTPS Only\nimpact: CRITICAL\nimpactDescription: \"Protects data in transit from interception\"\ntags: security, https, encryption, tls\n---\n\n## Enforce HTTPS Only\n\n**Impact: CRITICAL (Protects data in transit from interception)**\n\nAll API traffic must use HTTPS to encrypt data in transit. Never allow unencrypted HTTP connections for APIs.\n\n## Incorrect\n\n```javascript\n// ❌ HTTP server without TLS\nconst http = require('http');\nconst app = require('./app');\n\nhttp.createServer(app).listen(80, () => {\n console.log('API running on http://localhost:80');\n // Credentials transmitted in plain text!\n});\n\n// ❌ Optional HTTPS\nif (process.env.USE_HTTPS === 'true') {\n // HTTPS is optional, not enforced\n}\n\n// ❌ No redirect from HTTP to HTTPS\napp.get('/', (req, res) => {\n // Allows HTTP access\n res.json({ message: 'Welcome' });\n});\n\n// ❌ Insecure cookie settings\nres.cookie('session', token, {\n secure: false, // Sent over HTTP!\n httpOnly: true\n});\n```\n\n```json\n// ❌ HTTP URLs in responses\n{\n \"user\": {\n \"id\": 123,\n \"avatar\": \"http://cdn.example.com/avatar.jpg\",\n \"profile\": \"http://api.example.com/users/123\"\n }\n}\n```\n\n**Problems:**\n- Credentials and tokens transmitted in plain text can be intercepted\n- Man-in-the-middle attacks can modify data in transit\n- Cookies without the secure flag are sent over unencrypted connections\n- HTTP URLs in responses encourage insecure client behavior\n- Violates compliance requirements (PCI-DSS, HIPAA, GDPR)\n\n## Correct\n\n```javascript\n// ✅ HTTPS with security headers\nconst https = require('https');\nconst fs = require('fs');\nconst express = require('express');\nconst helmet = require('helmet');\n\nconst app = express();\n\n// Security headers including HSTS\napp.use(helmet({\n hsts: {\n maxAge: 31536000, // 1 year\n includeSubDomains: true,\n preload: true\n }\n}));\n\n// Force HTTPS redirect (for direct access)\napp.use((req, res, next) => {\n if (!req.secure && req.get('x-forwarded-proto') !== 'https') {\n return res.redirect(301, `https://${req.get('host')}${req.url}`);\n }\n next();\n});\n\n// Secure cookie settings\napp.use((req, res, next) => {\n res.cookie = function(name, value, options = {}) {\n options.secure = true; // HTTPS only\n options.httpOnly = true; // No JavaScript access\n options.sameSite = 'strict'; // CSRF protection\n return res.cookie.call(this, name, value, options);\n };\n next();\n});\n\n// HTTPS server\nconst options = {\n key: fs.readFileSync('/path/to/private.key'),\n cert: fs.readFileSync('/path/to/certificate.crt'),\n ca: fs.readFileSync('/path/to/ca-bundle.crt'),\n minVersion: 'TLSv1.2', // Minimum TLS version\n ciphers: [\n 'ECDHE-ECDSA-AES128-GCM-SHA256',\n 'ECDHE-RSA-AES128-GCM-SHA256',\n 'ECDHE-ECDSA-AES256-GCM-SHA384',\n 'ECDHE-RSA-AES256-GCM-SHA384'\n ].join(':')\n};\n\nhttps.createServer(options, app).listen(443, () => {\n console.log('Secure API running on https://localhost:443');\n});\n\n// Also listen on HTTP just to redirect\nconst http = require('http');\nhttp.createServer((req, res) => {\n res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });\n res.end();\n}).listen(80);\n```\n\n```python\n# ✅ FastAPI with HTTPS enforcement\nfrom fastapi import FastAPI, Request\nfrom fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware\nfrom fastapi.middleware.trustedhost import TrustedHostMiddleware\nfrom starlette.middleware.base import BaseHTTPMiddleware\n\napp = FastAPI()\n\n# Redirect HTTP to HTTPS\napp.add_middleware(HTTPSRedirectMiddleware)\n\n# Only allow specific hosts\napp.add_middleware(\n TrustedHostMiddleware,\n allowed_hosts=[\"api.example.com\", \"*.example.com\"]\n)\n\n# Add HSTS header\nclass HSTSMiddleware(BaseHTTPMiddleware):\n async def dispatch(self, request: Request, call_next):\n response = await call_next(request)\n response.headers[\"Strict-Transport-Security\"] = (\n \"max-age=31536000; includeSubDomains; preload\"\n )\n return response\n\napp.add_middleware(HSTSMiddleware)\n\n# Secure cookie response\nfrom fastapi.responses import JSONResponse\n\[email protected](\"/auth/login\")\nasync def login(credentials: Credentials):\n token = create_token(credentials)\n response = JSONResponse({\"status\": \"logged_in\"})\n response.set_cookie(\n key=\"session\",\n value=token,\n httponly=True,\n secure=True,\n samesite=\"strict\",\n max_age=86400\n )\n return response\n```\n\n```nginx\n# ✅ Nginx HTTPS configuration\nserver {\n listen 80;\n server_name api.example.com;\n return 301 https://$server_name$request_uri;\n}\n\nserver {\n listen 443 ssl http2;\n server_name api.example.com;\n\n ssl_certificate /etc/ssl/certs/certificate.crt;\n ssl_certificate_key /etc/ssl/private/private.key;\n\n # Modern TLS configuration\n ssl_protocols TLSv1.2 TLSv1.3;\n ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;\n ssl_prefer_server_ciphers on;\n\n # HSTS\n add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n\n # Security headers\n add_header X-Content-Type-Options \"nosniff\" always;\n add_header X-Frame-Options \"DENY\" always;\n add_header X-XSS-Protection \"1; mode=block\" always;\n\n location / {\n proxy_pass http://localhost:3000;\n proxy_set_header X-Forwarded-Proto https;\n }\n}\n```\n\n```yaml\n# ✅ Docker Compose with Traefik for automatic HTTPS\nversion: '3.8'\nservices:\n traefik:\n image: traefik:v2.10\n command:\n - \"--providers.docker=true\"\n - \"--entrypoints.web.address=:80\"\n - \"--entrypoints.websecure.address=:443\"\n - \"--entrypoints.web.http.redirections.entryPoint.to=websecure\"\n - \"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web\"\n - \"[email protected]\"\n - \"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json\"\n ports:\n - \"80:80\"\n - \"443:443\"\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - letsencrypt:/letsencrypt\n\n api:\n image: my-api\n labels:\n - \"traefik.http.routers.api.rule=Host(`api.example.com`)\"\n - \"traefik.http.routers.api.tls.certresolver=letsencrypt\"\n - \"traefik.http.middlewares.hsts.headers.stsSeconds=31536000\"\n - \"traefik.http.routers.api.middlewares=hsts\"\n```\n\n## TLS Configuration Checklist\n\n| Setting | Recommendation |\n|---------|----------------|\n| Minimum TLS | TLSv1.2 |\n| Preferred TLS | TLSv1.3 |\n| HSTS max-age | 31536000 (1 year) |\n| includeSubDomains | Yes |\n| HSTS preload | Yes (after testing) |\n| Certificate | Valid, not self-signed |\n| Certificate chain | Complete |\n\n**Benefits:**\n- HTTPS encrypts all data in transit, protecting credentials and sensitive data\n- TLS certificates verify server identity, preventing MITM attacks\n- Data integrity ensures content is not modified in transit\n- Meets compliance requirements for PCI-DSS, HIPAA, and GDPR\n- HTTP/2 and many modern web APIs require HTTPS\n- Secure cookies only work over HTTPS connections\n\nReference: [OWASP Transport Layer Protection](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7180,"content_sha256":"bc97d363d9ee10c2c0e638c38f7004eced2df3a5636c9ea0ed90f65b31937db4"},{"filename":"rules/sec-input-validation.md","content":"---\ntitle: Validate All Input Data\nimpact: CRITICAL\nimpactDescription: \"Prevents injection attacks and malformed data\"\ntags: security, validation, input-sanitization, injection\n---\n\n## Validate All Input Data\n\n**Impact: CRITICAL (Prevents injection attacks and malformed data)**\n\nNever trust client input. Validate, sanitize, and constrain all incoming data to prevent security vulnerabilities.\n\n## Incorrect\n\n```javascript\n// ❌ No validation\napp.post('/users', async (req, res) => {\n // Directly using user input!\n const user = await db.createUser(req.body);\n res.json(user);\n});\n\n// ❌ SQL injection vulnerability\napp.get('/users', async (req, res) => {\n const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;\n const users = await db.raw(query);\n res.json(users);\n});\n\n// ❌ NoSQL injection\napp.post('/login', async (req, res) => {\n const user = await db.users.findOne({\n email: req.body.email,\n password: req.body.password // Could be { $gt: '' }\n });\n});\n\n// ❌ Path traversal\napp.get('/files/:filename', (req, res) => {\n const path = `./uploads/${req.params.filename}`;\n res.sendFile(path); // Could access ../../../etc/passwd\n});\n```\n\n**Problems:**\n- SQL injection allows attackers to read, modify, or delete database data\n- NoSQL injection bypasses authentication with operator-based payloads\n- Path traversal exposes sensitive server files\n- Unbounded input sizes enable memory exhaustion and DoS attacks\n- Unsanitized HTML/script input enables XSS attacks\n- Type confusion attacks exploit loosely typed inputs\n\n## Correct\n\n```javascript\n// ✅ Comprehensive input validation\nconst { body, param, query, validationResult } = require('express-validator');\nconst sanitizeHtml = require('sanitize-html');\nconst path = require('path');\n\n// Validation middleware\nconst validateUser = [\n body('email')\n .trim()\n .notEmpty().withMessage('Email is required')\n .isEmail().withMessage('Must be a valid email')\n .normalizeEmail()\n .isLength({ max: 255 }).withMessage('Email too long'),\n\n body('password')\n .notEmpty().withMessage('Password is required')\n .isLength({ min: 8, max: 100 }).withMessage('Password must be 8-100 characters')\n .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/).withMessage('Password too weak'),\n\n body('name')\n .trim()\n .notEmpty().withMessage('Name is required')\n .isLength({ min: 1, max: 100 }).withMessage('Name must be 1-100 characters')\n .matches(/^[a-zA-Z\\s'-]+$/).withMessage('Name contains invalid characters')\n .customSanitizer(value => sanitizeHtml(value, { allowedTags: [] })),\n\n body('age')\n .optional()\n .isInt({ min: 0, max: 150 }).withMessage('Age must be 0-150')\n .toInt(),\n\n body('website')\n .optional()\n .trim()\n .isURL({ protocols: ['http', 'https'] }).withMessage('Invalid URL')\n];\n\n// Validation error handler\nconst handleValidation = (req, res, next) => {\n const errors = validationResult(req);\n if (!errors.isEmpty()) {\n return res.status(400).json({\n error: {\n code: 'validation_error',\n message: 'Invalid input data',\n details: errors.array().map(e => ({\n field: e.path,\n message: e.msg,\n value: e.value\n }))\n }\n });\n }\n next();\n};\n\napp.post('/users', validateUser, handleValidation, async (req, res) => {\n const user = await db.createUser(req.body);\n res.status(201).json(user);\n});\n\n// ✅ Parameterized queries (prevent SQL injection)\napp.get('/users', async (req, res) => {\n const users = await db.query(\n 'SELECT id, name, email FROM users WHERE name = ?',\n [req.query.name]\n );\n res.json(users);\n});\n\n// ✅ Safe MongoDB queries\napp.post('/login', async (req, res) => {\n // Ensure email and password are strings\n const email = String(req.body.email || '');\n const password = String(req.body.password || '');\n\n const user = await db.users.findOne({ email });\n if (!user || !await bcrypt.compare(password, user.passwordHash)) {\n return res.status(401).json({ error: 'Invalid credentials' });\n }\n // ...\n});\n\n// ✅ Path traversal prevention\napp.get('/files/:filename',\n param('filename')\n .matches(/^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9]+$/)\n .withMessage('Invalid filename'),\n handleValidation,\n (req, res) => {\n const uploadsDir = path.resolve('./uploads');\n const filePath = path.join(uploadsDir, req.params.filename);\n\n // Ensure path is within uploads directory\n if (!filePath.startsWith(uploadsDir)) {\n return res.status(400).json({ error: 'Invalid path' });\n }\n\n res.sendFile(filePath);\n }\n);\n\n// ✅ Array size limits\napp.post('/batch',\n body('items')\n .isArray({ min: 1, max: 100 })\n .withMessage('Items must be array of 1-100 elements'),\n body('items.*.id')\n .isUUID()\n .withMessage('Each item must have valid UUID'),\n handleValidation,\n async (req, res) => {\n const results = await processBatch(req.body.items);\n res.json(results);\n }\n);\n\n// ✅ JSON depth/size limits\nconst jsonParser = express.json({\n limit: '100kb', // Max request body size\n strict: true // Only accept arrays and objects\n});\n\napp.use('/api', jsonParser);\n```\n\n```python\n# ✅ FastAPI with Pydantic validation\nfrom fastapi import FastAPI, Query, Path, Body, HTTPException\nfrom pydantic import BaseModel, EmailStr, Field, validator, HttpUrl\nfrom typing import Optional, List\nimport re\nimport bleach\n\napp = FastAPI()\n\nclass UserCreate(BaseModel):\n email: EmailStr\n password: str = Field(..., min_length=8, max_length=100)\n name: str = Field(..., min_length=1, max_length=100)\n age: Optional[int] = Field(None, ge=0, le=150)\n website: Optional[HttpUrl] = None\n\n @validator('password')\n def password_strength(cls, v):\n if not re.search(r'[A-Z]', v):\n raise ValueError('Must contain uppercase letter')\n if not re.search(r'[a-z]', v):\n raise ValueError('Must contain lowercase letter')\n if not re.search(r'\\d', v):\n raise ValueError('Must contain digit')\n return v\n\n @validator('name')\n def sanitize_name(cls, v):\n # Only allow letters, spaces, hyphens, apostrophes\n if not re.match(r\"^[a-zA-Z\\s'-]+$\", v):\n raise ValueError('Name contains invalid characters')\n # Sanitize HTML\n return bleach.clean(v, tags=[], strip=True)\n\nclass BatchRequest(BaseModel):\n items: List[str] = Field(..., min_items=1, max_items=100)\n\n @validator('items', each_item=True)\n def validate_uuid(cls, v):\n if not re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}

API Design Patterns RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation. Metadata - Version: 2.0.0 - Rule Count: 38 rules across 7 categories - License: MIT When to Apply Reference these guidelines when: - Designing new API endpoints - Reviewing existing API structure - Implementing error handling and validation - Setting up pagination, filtering, and sorting - Planning API versioning strategy - Configuring API securit…

, v, re.I):\n raise ValueError('Invalid UUID format')\n return v\n\[email protected](\"/users\")\nasync def create_user(user: UserCreate):\n return await db.create_user(user.dict())\n\[email protected](\"/users/{user_id}\")\nasync def get_user(\n user_id: int = Path(..., ge=1, le=2147483647, description=\"User ID\")\n):\n return await db.get_user(user_id)\n\[email protected](\"/search\")\nasync def search(\n q: str = Query(..., min_length=1, max_length=100),\n page: int = Query(1, ge=1, le=1000),\n limit: int = Query(20, ge=1, le=100)\n):\n # q is automatically validated and constrained\n return await db.search(q, page, limit)\n```\n\n## Input Validation Checklist\n\n| Check | Why |\n|-------|-----|\n| Type validation | Prevent type confusion attacks |\n| Length limits | Prevent buffer overflows, DoS |\n| Character whitelist | Prevent injection attacks |\n| Range validation | Ensure business logic integrity |\n| Format validation | Email, URL, UUID patterns |\n| Sanitization | Remove/escape dangerous content |\n| Array size limits | Prevent memory exhaustion |\n| Nested depth limits | Prevent stack overflow |\n\n**Benefits:**\n- Prevents SQL/NoSQL injection attacks from manipulating queries\n- Sanitizing input stops cross-site scripting (XSS) attacks\n- Filename validation prevents unauthorized file access via path traversal\n- Size limits prevent memory exhaustion and denial-of-service attacks\n- Ensures only valid data enters your system, maintaining data integrity\n- Catches bad data at the API boundary before it causes downstream errors\n\nReference: [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8239,"content_sha256":"e4bbecf72a0180921eb439bc821079bf45a7e38092d305d107f4d13631d9998b"},{"filename":"rules/sec-rate-limiting.md","content":"---\ntitle: Implement Rate Limiting\nimpact: CRITICAL\nimpactDescription: \"Prevents abuse and ensures service availability\"\ntags: security, rate-limiting, abuse-prevention, throttling\n---\n\n## Implement Rate Limiting\n\n**Impact: CRITICAL (Prevents abuse and ensures service availability)**\n\nProtect your API from abuse by limiting the number of requests clients can make within a time window.\n\n## Incorrect\n\n```javascript\n// ❌ No rate limiting\napp.post('/login', async (req, res) => {\n // Vulnerable to brute force attacks\n const user = await authenticate(req.body);\n res.json(user);\n});\n\n// ❌ Rate limit only on response\napp.get('/api/data', async (req, res) => {\n const data = await expensiveQuery(); // Query runs every time!\n if (requestCount > 100) {\n res.status(429).json({ error: 'Too many requests' });\n }\n res.json(data);\n});\n\n// ❌ No rate limit headers\napp.use((req, res, next) => {\n if (isRateLimited(req)) {\n res.status(429).send('Too many requests');\n // Client doesn't know when to retry\n }\n next();\n});\n```\n\n**Problems:**\n- Login endpoints without rate limits are vulnerable to brute force attacks\n- Checking limits after processing wastes server resources\n- Missing retry headers leave clients guessing when they can retry\n- No per-user or per-endpoint differentiation allows abuse\n- Expensive operations run before limits are checked\n\n## Correct\n\n```javascript\n// ✅ Comprehensive rate limiting\nconst rateLimit = require('express-rate-limit');\nconst RedisStore = require('rate-limit-redis');\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\n\n// General API rate limiter\nconst apiLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:api:'\n }),\n windowMs: 60 * 1000, // 1 minute\n max: 100, // 100 requests per minute\n standardHeaders: true, // Return rate limit info in headers\n legacyHeaders: false,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Too many requests. Please slow down.',\n retryAfter: 60\n }\n },\n keyGenerator: (req) => {\n // Rate limit by user ID if authenticated, otherwise by IP\n return req.user?.id || req.ip;\n }\n});\n\n// Stricter limiter for auth endpoints\nconst authLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:auth:'\n }),\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: 5, // 5 attempts per 15 minutes\n standardHeaders: true,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Too many login attempts. Please try again later.',\n retryAfter: 900\n }\n },\n keyGenerator: (req) => req.body.email || req.ip // Per email address\n});\n\n// Expensive endpoint limiter\nconst expensiveLimiter = rateLimit({\n store: new RedisStore({\n client: redis,\n prefix: 'rl:expensive:'\n }),\n windowMs: 60 * 60 * 1000, // 1 hour\n max: 10, // 10 requests per hour\n standardHeaders: true,\n message: {\n error: {\n code: 'rate_limit_exceeded',\n message: 'Export limit reached. Try again in an hour.',\n retryAfter: 3600\n }\n }\n});\n\n// Apply rate limiters\napp.use('/api/', apiLimiter);\napp.use('/auth/login', authLimiter);\napp.use('/auth/password-reset', authLimiter);\napp.post('/api/export', expensiveLimiter, exportHandler);\n\n// Custom sliding window implementation\nclass SlidingWindowRateLimiter {\n constructor(redis, options) {\n this.redis = redis;\n this.windowMs = options.windowMs;\n this.maxRequests = options.max;\n }\n\n async isAllowed(key) {\n const now = Date.now();\n const windowStart = now - this.windowMs;\n\n const pipeline = this.redis.pipeline();\n\n // Remove old entries\n pipeline.zremrangebyscore(key, 0, windowStart);\n\n // Count requests in window\n pipeline.zcard(key);\n\n // Add current request\n pipeline.zadd(key, now, `${now}-${Math.random()}`);\n\n // Set expiry\n pipeline.expire(key, Math.ceil(this.windowMs / 1000));\n\n const results = await pipeline.exec();\n const count = results[1][1];\n\n return {\n allowed: count \u003c this.maxRequests,\n remaining: Math.max(0, this.maxRequests - count - 1),\n resetAt: new Date(now + this.windowMs)\n };\n }\n}\n\n// Tiered rate limiting based on plan\nconst tierLimits = {\n free: { requestsPerMinute: 60, requestsPerDay: 1000 },\n basic: { requestsPerMinute: 300, requestsPerDay: 10000 },\n pro: { requestsPerMinute: 1000, requestsPerDay: 100000 },\n enterprise: { requestsPerMinute: 5000, requestsPerDay: 1000000 }\n};\n\nasync function tieredRateLimiter(req, res, next) {\n const user = req.user;\n const tier = user?.plan || 'free';\n const limits = tierLimits[tier];\n\n const minuteKey = `rl:${user?.id || req.ip}:minute`;\n const dayKey = `rl:${user?.id || req.ip}:day`;\n\n // Check minute limit\n const minuteCount = await redis.incr(minuteKey);\n if (minuteCount === 1) {\n await redis.expire(minuteKey, 60);\n }\n\n // Check daily limit\n const dayCount = await redis.incr(dayKey);\n if (dayCount === 1) {\n await redis.expire(dayKey, 86400);\n }\n\n // Set rate limit headers\n res.set('X-RateLimit-Limit', limits.requestsPerMinute);\n res.set('X-RateLimit-Remaining', Math.max(0, limits.requestsPerMinute - minuteCount));\n res.set('X-RateLimit-Reset', Math.ceil(Date.now() / 1000) + 60);\n\n if (minuteCount > limits.requestsPerMinute) {\n return res.status(429).json({\n error: {\n code: 'rate_limit_exceeded',\n message: 'Minute rate limit exceeded',\n limit: limits.requestsPerMinute,\n window: '1 minute',\n retryAfter: await redis.ttl(minuteKey)\n }\n });\n }\n\n if (dayCount > limits.requestsPerDay) {\n return res.status(429).json({\n error: {\n code: 'rate_limit_exceeded',\n message: 'Daily rate limit exceeded',\n limit: limits.requestsPerDay,\n window: '24 hours',\n upgradeUrl: '/pricing'\n }\n });\n }\n\n next();\n}\n```\n\n```python\n# ✅ FastAPI with rate limiting\nfrom fastapi import FastAPI, Request, HTTPException\nfrom slowapi import Limiter, _rate_limit_exceeded_handler\nfrom slowapi.util import get_remote_address\nfrom slowapi.errors import RateLimitExceeded\n\nlimiter = Limiter(key_func=get_remote_address)\napp = FastAPI()\napp.state.limiter = limiter\napp.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)\n\[email protected](\"/api/data\")\[email protected](\"100/minute\")\nasync def get_data(request: Request):\n return {\"data\": \"value\"}\n\[email protected](\"/auth/login\")\[email protected](\"5/15minutes\")\nasync def login(request: Request):\n return {\"token\": \"...\"}\n\n# Custom user-based rate limiting\nasync def get_rate_limit_key(request: Request):\n if request.user:\n return f\"user:{request.user.id}\"\n return f\"ip:{request.client.host}\"\n\[email protected](\"/api/premium\")\[email protected](\"1000/minute\", key_func=get_rate_limit_key)\nasync def premium_endpoint(request: Request):\n return {\"data\": \"premium\"}\n```\n\n## Rate Limit Headers\n\n```\nHTTP/1.1 200 OK\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 95\nX-RateLimit-Reset: 1705312800\nRetry-After: 60\n```\n\n**Benefits:**\n- Prevents denial-of-service attacks from overwhelming servers\n- Limits brute force password guessing and credential stuffing attacks\n- Ensures fair API access for all users\n- Prevents runaway usage that increases infrastructure costs\n- Enables tiered pricing based on usage limits\n- Rate limit headers help clients implement proper backoff strategies\n\nReference: [IETF Rate Limiting Headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7510,"content_sha256":"fec840616206347a7e0b914828ae1ae440a62bf3e6bfa19688a32462547b21c4"},{"filename":"rules/sec-sensitive-data.md","content":"---\ntitle: Protect Sensitive Data in Responses\nimpact: CRITICAL\nimpactDescription: \"Prevents data leaks and privacy violations\"\ntags: security, privacy, sensitive-data, pii\n---\n\n## Protect Sensitive Data in Responses\n\n**Impact: CRITICAL (Prevents data leaks and privacy violations)**\n\nNever expose sensitive information like passwords, tokens, internal IDs, or PII in API responses.\n\n## Incorrect\n\n```json\n// ❌ Exposing password hash\n{\n \"id\": 123,\n \"email\": \"[email protected]\",\n \"passwordHash\": \"$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.n\",\n \"name\": \"John Doe\"\n}\n\n// ❌ Exposing API keys\n{\n \"user\": {\n \"id\": 123,\n \"apiKey\": \"sk_live_abc123xyz789\",\n \"secretKey\": \"whsec_secret_key_here\"\n }\n}\n\n// ❌ Exposing internal IDs\n{\n \"user\": {\n \"id\": 123,\n \"internalDatabaseId\": \"mongo_507f1f77bcf86cd799439011\",\n \"stripeCustomerId\": \"cus_abc123\"\n }\n}\n\n// ❌ Exposing other users' data\n{\n \"order\": {\n \"id\": 456,\n \"customer\": {\n \"ssn\": \"123-45-6789\",\n \"creditCard\": \"4111111111111111\",\n \"dateOfBirth\": \"1990-01-15\"\n }\n }\n}\n```\n\n```javascript\n// ❌ Returning entire database record\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json(user); // Exposes ALL fields including sensitive ones\n});\n```\n\n**Problems:**\n- Exposed password hashes enable offline brute force attacks\n- API keys in responses allow unauthorized access to third-party services\n- Internal IDs help attackers enumerate resources and map infrastructure\n- PII exposure violates privacy regulations (GDPR, CCPA, PCI-DSS)\n- Returning full database records exposes fields never intended for clients\n\n## Correct\n\n```javascript\n// ✅ Explicit field selection per context\nconst userPublicFields = ['id', 'name', 'avatar', 'createdAt'];\nconst userPrivateFields = [...userPublicFields, 'email', 'settings'];\nconst userAdminFields = [...userPrivateFields, 'roles', 'lastLoginAt', 'status'];\n\nfunction sanitizeUser(user, context = 'public') {\n const allowedFields = {\n public: userPublicFields,\n private: userPrivateFields,\n admin: userAdminFields\n }[context];\n\n return Object.fromEntries(\n Object.entries(user).filter(([key]) => allowedFields.includes(key))\n );\n}\n\n// Use explicit field selection\napp.get('/users/:id', authenticate, async (req, res) => {\n const user = await db.findUser(req.params.id);\n\n if (!user) {\n return res.status(404).json({ error: 'User not found' });\n }\n\n // Determine context based on requester\n let context = 'public';\n if (req.user.id === user.id) {\n context = 'private';\n } else if (req.user.roles.includes('admin')) {\n context = 'admin';\n }\n\n res.json(sanitizeUser(user, context));\n});\n\n// Use DTOs/serializers\nclass UserResponse {\n constructor(user, includePrivate = false) {\n this.id = user.id;\n this.name = user.name;\n this.avatar = user.avatar;\n this.createdAt = user.createdAt;\n\n if (includePrivate) {\n this.email = user.email;\n this.settings = user.settings;\n }\n }\n}\n\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n const includePrivate = req.user?.id === user.id;\n res.json(new UserResponse(user, includePrivate));\n});\n\n// Mask sensitive data\nfunction maskEmail(email) {\n const [local, domain] = email.split('@');\n const maskedLocal = local[0] + '*'.repeat(local.length - 2) + local.slice(-1);\n return `${maskedLocal}@${domain}`;\n}\n\nfunction maskPhone(phone) {\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n}\n\nfunction maskCreditCard(number) {\n return '**** **** **** ' + number.slice(-4);\n}\n\n// Apply masking in responses\napp.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n\n res.json({\n id: user.id,\n name: user.name,\n email: req.user.id === user.id ? user.email : maskEmail(user.email),\n phone: maskPhone(user.phone),\n paymentMethod: {\n type: 'card',\n last4: user.creditCard.slice(-4),\n brand: user.cardBrand\n }\n });\n});\n```\n\n```python\n# ✅ FastAPI with Pydantic response models\nfrom pydantic import BaseModel, EmailStr, Field\nfrom typing import Optional\n\n# Internal model (full data)\nclass UserInternal(BaseModel):\n id: int\n email: str\n password_hash: str\n name: str\n ssn: Optional[str]\n api_key: str\n stripe_customer_id: str\n\n# Public response model (safe to expose)\nclass UserPublic(BaseModel):\n id: int\n name: str\n avatar_url: Optional[str]\n\n class Config:\n # Only include fields defined in this model\n extra = 'forbid'\n\n# Private response model (for account owner)\nclass UserPrivate(UserPublic):\n email: EmailStr\n settings: dict\n\n# Admin response model\nclass UserAdmin(UserPrivate):\n status: str\n roles: list\n last_login_at: Optional[str]\n\[email protected](\"/users/{user_id}\", response_model=UserPublic)\nasync def get_user(user_id: int, current_user = Depends(get_current_user)):\n user = await db.get_user(user_id)\n\n if current_user.id == user_id:\n return UserPrivate(**user.dict())\n elif \"admin\" in current_user.roles:\n return UserAdmin(**user.dict())\n else:\n return UserPublic(**user.dict())\n\n# Mask sensitive data in logs\nimport logging\n\nclass SensitiveDataFilter(logging.Filter):\n PATTERNS = [\n (r'\"password\":\\s*\"[^\"]*\"', '\"password\": \"[REDACTED]\"'),\n (r'\"token\":\\s*\"[^\"]*\"', '\"token\": \"[REDACTED]\"'),\n (r'\"apiKey\":\\s*\"[^\"]*\"', '\"apiKey\": \"[REDACTED]\"'),\n (r'\\b\\d{3}-\\d{2}-\\d{4}\\b', '[SSN REDACTED]'),\n (r'\\b\\d{16}\\b', '[CARD REDACTED]'),\n ]\n\n def filter(self, record):\n import re\n message = record.getMessage()\n for pattern, replacement in self.PATTERNS:\n message = re.sub(pattern, replacement, message)\n record.msg = message\n return True\n```\n\n```json\n// ✅ Safe user response\n{\n \"id\": 123,\n \"name\": \"John Doe\",\n \"avatar\": \"https://cdn.example.com/avatars/123.jpg\",\n \"email\": \"j***[email protected]\",\n \"memberSince\": \"2023-01-15\"\n}\n\n// ✅ Safe payment method response\n{\n \"paymentMethods\": [\n {\n \"id\": \"pm_abc123\",\n \"type\": \"card\",\n \"brand\": \"visa\",\n \"last4\": \"4242\",\n \"expiryMonth\": 12,\n \"expiryYear\": 2025\n }\n ]\n}\n```\n\n## Sensitive Data Checklist\n\n| Field | Action | Reason |\n|-------|--------|--------|\n| Password hash | Never expose | Security |\n| API keys | Never expose | Security |\n| SSN/Tax ID | Never/mask | PII/Compliance |\n| Full credit card | Never expose | PCI-DSS |\n| Internal IDs | Usually hide | Information disclosure |\n| Email (other users) | Mask | Privacy |\n| Phone | Mask | Privacy |\n| Address | Context-dependent | Privacy |\n\n**Benefits:**\n- Prevents account takeover from exposed credentials\n- Meets privacy regulation requirements (GDPR, CCPA, PCI-DSS)\n- Reduces attack surface by hiding internal system identifiers\n- Context-based field exposure follows the principle of least privilege\n- Data masking provides useful info without exposing full values\n- Simplifies compliance audits with clear data handling policies\n\nReference: [OWASP Sensitive Data Exposure](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/09-Testing_for_Weak_Cryptography/04-Testing_for_Weak_Encryption)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7283,"content_sha256":"2cddc08c10c684812b3813c0226b499915eaf8e7a50700399ccb449ce0ea6d6f"},{"filename":"rules/sort-flexible.md","content":"---\ntitle: Flexible Sorting Options\nimpact: HIGH\nimpactDescription: \"Empowers clients to retrieve data in the exact order they need without server-side changes\"\ntags: sorting, query-params, ordering\n---\n\n## Flexible Sorting Options\n\n**Impact: HIGH (Empowers clients to retrieve data in the exact order they need without server-side changes)**\n\nAPIs that return data in a single hardcoded order force clients to re-sort in memory, which is wasteful and breaks pagination. A flexible sorting API lets clients request the order they need, and the database handles it efficiently using indexes.\n\n## Incorrect\n\n```http\n# No sort parameter — always returns by id ASC\nGET /api/v1/products\n```\n\n```json\n[\n { \"id\": 1, \"name\": \"Alpha\", \"price\": 29.99, \"created_at\": \"2023-01-01T00:00:00Z\" },\n { \"id\": 2, \"name\": \"Beta\", \"price\": 9.99, \"created_at\": \"2024-06-15T00:00:00Z\" }\n]\n```\n\n```http\n# Or: inconsistent sort parameters across endpoints\nGET /api/v1/products?order_by=price&direction=desc\nGET /api/v1/users?sortField=name&sortOrder=asc\n```\n\n**Problems:**\n- Client must fetch all data and sort in memory, defeating the purpose of pagination\n- No way to get \"newest first\" or \"cheapest first\" without client-side processing\n- Inconsistent sort parameter names across endpoints increase integration complexity\n- Hardcoded order may not match any client's primary use case\n\n## Correct\n\n```http\n# Ascending sort (default direction)\nGET /api/v1/products?sort=price\n\n# Descending sort with \"-\" prefix\nGET /api/v1/products?sort=-created_at\n\n# Multiple sort fields (comma-separated)\nGET /api/v1/products?sort=category,-price\n\n# Combined with filtering and pagination\nGET /api/v1/products?status=active&sort=-created_at&page=1&per_page=20\n```\n\n```json\n{\n \"data\": [\n { \"id\": 47, \"name\": \"New Widget\", \"price\": 59.99, \"created_at\": \"2024-06-15T00:00:00Z\" },\n { \"id\": 32, \"name\": \"Another Widget\", \"price\": 39.99, \"created_at\": \"2024-05-10T00:00:00Z\" }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"per_page\": 20,\n \"total_count\": 84,\n \"sort\": \"-created_at\"\n }\n}\n```\n\n**Benefits:**\n- `-` prefix convention for descending is compact and widely adopted (JSON:API, many major APIs)\n- Multiple sort fields let clients express complex ordering like \"category ascending, then price descending\"\n- Sorting happens at the database level where indexes make it efficient\n- Consistent `sort` parameter name across all endpoints simplifies client SDKs\n\nReference: [JSON:API - Sorting](https://jsonapi.org/format/#fetching-sorting)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2514,"content_sha256":"234524b5d6fa3ec74bc5074141fbc6931027a29ed81cc275cd7b3fff05ff0a9f"},{"filename":"rules/ver-backward-compatible.md","content":"---\ntitle: Maintain Backward Compatibility\nimpact: HIGH\nimpactDescription: \"Prevents breaking existing integrations and avoids costly emergency client fixes\"\ntags: versioning, backward-compatibility, breaking-changes\n---\n\n## Maintain Backward Compatibility\n\n**Impact: HIGH (Prevents breaking existing integrations and avoids costly emergency client fixes)**\n\nBreaking changes in a live API force every consumer to update simultaneously or face outages. Maintaining backward compatibility within a version means clients continue working after deployments, and new features are delivered through additive changes only. Reserve breaking changes for new major versions.\n\n## Incorrect\n\n```http\n# Before: GET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"role\": \"admin\"\n}\n```\n\n```http\n# After deploy (same v1): field renamed, field removed, type changed\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"roles\": [\"admin\", \"editor\"]\n}\n```\n\n**Problems:**\n- Renaming `name` to `full_name` breaks every client reading `response.name`\n- Renaming `email` to `email_address` breaks form bindings and display logic\n- Changing `role` (string) to `roles` (array) causes type errors in client deserialization\n- Removing fields with no notice gives consumers zero time to adapt\n\n## Correct\n\n```http\n# Additive changes only within v1: new fields added, old fields preserved\nGET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"full_name\": \"Jane Smith\",\n \"email\": \"[email protected]\",\n \"email_address\": \"[email protected]\",\n \"role\": \"admin\",\n \"roles\": [\"admin\", \"editor\"],\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n}\n```\n\n```http\n# Deprecation communicated via response headers\nHTTP/1.1 200 OK\nContent-Type: application/json\nX-Deprecated-Fields: name, email, role\n```\n\n```http\n# New endpoints are always safe to add\nGET /api/v1/users/1/preferences # new endpoint, no existing contract\n```\n\n**Benefits:**\n- Existing clients continue working without any code changes after every deploy\n- New clients can adopt new field names immediately while old names remain available\n- Deprecation headers give automated tooling a way to detect and flag stale usage\n- New endpoints and new fields never conflict with existing client expectations\n\nReference: [Stripe API - Backward Compatibility](https://stripe.com/docs/upgrades)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2435,"content_sha256":"c41eb15a530f3319dfc92e6b4ab46b957207b36b37a4f2e1fa32b4c91991165f"},{"filename":"rules/ver-deprecation.md","content":"---\ntitle: Deprecation Strategy\nimpact: HIGH\nimpactDescription: \"Structured deprecation prevents surprise outages and gives consumers a clear migration path\"\ntags: versioning, deprecation, sunset, migration\n---\n\n## Deprecation Strategy\n\n**Impact: HIGH (Structured deprecation prevents surprise outages and gives consumers a clear migration path)**\n\nRemoving an API version or endpoint without advance notice breaks consumer trust and causes production outages. A proper deprecation strategy uses standard HTTP headers, documented timelines, and migration guides to give consumers the time and information they need to transition safely.\n\n## Incorrect\n\n```http\n# Monday: API v1 is working\nGET /api/v1/users\nHTTP/1.1 200 OK\n\n# Tuesday: API v1 is gone without warning\nGET /api/v1/users\nHTTP/1.1 404 Not Found\n\n{\n \"error\": \"This endpoint no longer exists\"\n}\n```\n\n**Problems:**\n- Consumers discover the removal through production failures, not proactive communication\n- No migration period means every consumer must update simultaneously or break\n- No documentation of what changed or where to go creates confusion and support burden\n- Erodes trust — consumers cannot rely on the API for production workloads\n\n## Correct\n\n```http\n# Phase 1: Announce deprecation (minimum 6 months before removal)\nGET /api/v1/users\nHTTP/1.1 200 OK\nDeprecation: true\nSunset: Sat, 01 Jun 2025 00:00:00 GMT\nLink: \u003c/api/v2/migration-guide>; rel=\"deprecation\"\n```\n\n```json\n{\n \"data\": [\n { \"id\": 1, \"name\": \"Jane Smith\" }\n ],\n \"_deprecation\": {\n \"message\": \"API v1 is deprecated. Please migrate to v2 by June 1, 2025.\",\n \"migration_guide\": \"https://api.example.com/docs/v1-to-v2-migration\",\n \"sunset_date\": \"2025-06-01T00:00:00Z\"\n }\n}\n```\n\n```http\n# Phase 2: After sunset date — return 410 Gone (not 404)\nGET /api/v1/users\nHTTP/1.1 410 Gone\n\n{\n \"error\": {\n \"code\": \"API_VERSION_SUNSET\",\n \"message\": \"API v1 was removed on June 1, 2025. Please use /api/v2/users.\",\n \"migration_guide\": \"https://api.example.com/docs/v1-to-v2-migration\",\n \"current_version\": \"/api/v2/users\"\n }\n}\n```\n\n```\nDeprecation Timeline:\n T-6 months: Add Deprecation + Sunset headers, publish migration guide\n T-3 months: Send email/notification to API consumers\n T-1 month: Final reminder, log consumers still using deprecated version\n T-0: Return 410 Gone with redirect information\n T+3 months: Remove deprecated code from codebase\n```\n\n**Benefits:**\n- `Sunset` header (RFC 8594) is a machine-readable standard that monitoring tools can detect automatically\n- `410 Gone` correctly signals permanent removal (unlike `404` which implies the resource may return)\n- Migration guide link in both headers and body ensures consumers can find upgrade instructions\n- Minimum 6-month window gives teams of all sizes enough time to plan and execute migration\n\nReference: [RFC 8594 - The Sunset HTTP Header Field](https://datatracker.ietf.org/doc/html/rfc8594)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2939,"content_sha256":"7c06f6551274aa7b477e45503dcad1e2a1581c0e59f1ae1df57558cf5906a748"},{"filename":"rules/ver-header-based.md","content":"---\ntitle: Version via Accept Header\nimpact: HIGH\nimpactDescription: \"Clean URLs with content negotiation-based versioning for API evolution\"\ntags: versioning, headers, content-negotiation\n---\n\n## Version via Accept Header\n\n**Impact: HIGH (Clean URLs with content negotiation-based versioning for API evolution)**\n\nHeader-based versioning uses the `Accept` header to specify the desired API version, keeping URLs clean and resource-centric. This approach aligns with HTTP content negotiation semantics and is preferred when URL aesthetics matter or when the same resource should be accessible across versions without changing its canonical URL.\n\n## Incorrect\n\n```http\n# No version information at all — client gets whatever the current version is\nGET /api/users/1\nAccept: application/json\n```\n\n```http\n# Or: custom non-standard header\nGET /api/users/1\nX-API-Version: 2\n```\n\n**Problems:**\n- No standard mechanism to request a specific version of the response format\n- Custom headers are not part of HTTP content negotiation and may be stripped by proxies\n- Clients have no guarantee about the response shape they will receive\n- `X-` prefixed headers are deprecated by RFC 6648 and signal non-standard behavior\n\n## Correct\n\n```http\n# Request version 1\nGET /api/users/1\nAccept: application/vnd.myapi.v1+json\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```http\n# Request version 2\nGET /api/users/1\nAccept: application/vnd.myapi.v2+json\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"profile\": {\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n }\n}\n```\n\n```http\n# Server responds with matching Content-Type\nHTTP/1.1 200 OK\nContent-Type: application/vnd.myapi.v2+json\n```\n\n| Aspect | URL Path (`/v1/`) | Accept Header |\n|---|---|---|\n| Visibility | Version in every URL | Hidden in headers |\n| Caching | Simple (URL-based) | Requires `Vary: Accept` |\n| Browser testing | Easy (type URL) | Needs tool (curl, Postman) |\n| URL cleanliness | Extra path segment | Clean resource URLs |\n| HTTP semantics | Convention-based | Proper content negotiation |\n\n**Benefits:**\n- URLs remain clean and resource-focused — `/api/users/1` is the canonical identifier\n- Follows HTTP content negotiation standards (`Accept` / `Content-Type`)\n- Server can default to the latest version when no version header is sent\n- Multiple representations of the same resource at the same URL, which aligns with REST principles\n\nReference: [GitHub API - Media Types](https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#accept)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2628,"content_sha256":"e90741df3430940c5f937b160265755f3f33d729f09d185d5413faaf1e7d9da8"},{"filename":"rules/ver-url-path.md","content":"---\ntitle: Version in URL Path\nimpact: HIGH\nimpactDescription: \"Explicit version visibility prevents accidental breaking-change consumption\"\ntags: versioning, url-path, api-version\n---\n\n## Version in URL Path\n\n**Impact: HIGH (Explicit version visibility prevents accidental breaking-change consumption)**\n\nURL path versioning is the most explicit and widely adopted approach to API versioning. The version is visible in every request, making it impossible to accidentally hit the wrong version. It works naturally with routing, caching, load balancing, and documentation tools.\n\n## Incorrect\n\n```http\n# No version — all consumers share one contract\nGET /api/users\n```\n\n```json\n// v1 response shape\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```json\n// After a breaking change — same URL, different shape\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"name\": null\n}\n```\n\n**Problems:**\n- Breaking changes immediately affect all consumers with no migration path\n- No way to run old and new versions side-by-side during transition periods\n- Clients cannot pin to a known-good contract — any deploy can break them\n- Rollback requires reverting the entire API, not just routing rules\n\n## Correct\n\n```http\n# Version 1 — original contract\nGET /api/v1/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"name\": \"Jane Smith\",\n \"email\": \"[email protected]\"\n}\n```\n\n```http\n# Version 2 — new contract, coexists with v1\nGET /api/v2/users/1\n```\n\n```json\n{\n \"id\": 1,\n \"full_name\": \"Jane Smith\",\n \"email_address\": \"[email protected]\",\n \"profile\": {\n \"avatar_url\": \"https://cdn.example.com/avatars/1.jpg\"\n }\n}\n```\n\n**Benefits:**\n- Version is immediately visible in URLs, logs, and documentation\n- Old and new versions run simultaneously — consumers migrate at their own pace\n- Load balancers and API gateways can route versions to different backends\n- CDNs and proxies cache each version independently without conflict\n\nReference: [Stripe API - Versioning](https://stripe.com/docs/api/versioning)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2051,"content_sha256":"fbf84039aec2c0ddb0943594ea64b327ae970cda47ca3735579a571fb3444643"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"API Design Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Metadata","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Version:","type":"text","marks":[{"type":"strong"}]},{"text":" 2.0.0","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rule Count:","type":"text","marks":[{"type":"strong"}]},{"text":" 38 rules across 7 categories","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"License:","type":"text","marks":[{"type":"strong"}]},{"text":" MIT","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference these guidelines when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Designing new API endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing existing API structure","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing error handling and validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up pagination, filtering, and sorting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Planning API versioning strategy","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Configuring API security (auth, CORS, rate limiting)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Writing API documentation (OpenAPI/Swagger)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Categories by Priority","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prefix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resource Design","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rest-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"error-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sec-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pagination & Filtering","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"page-","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"filter-","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"sort-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Versioning","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ver-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response Format","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"resp-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Documentation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"doc-","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Resource Design (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-nouns-not-verbs","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use nouns for endpoints, not verbs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-plural-resources","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use plural resource names","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-http-methods","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Correct HTTP method usage (GET, POST, PUT, PATCH, DELETE)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-nested-resources","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Proper resource nesting (max 2 levels)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-status-codes","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Appropriate HTTP status codes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-idempotency","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Idempotent operations with idempotency keys","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-hateoas","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Hypermedia links for discoverability","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rest-resource-actions","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Non-CRUD actions as sub-resources","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Error Handling (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-consistent-format","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Consistent error response structure","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-meaningful-messages","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Helpful, actionable error messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-validation-details","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Field-level validation errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-error-codes","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Machine-readable error codes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-no-stack-traces","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Never expose stack traces in production","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"error-request-id","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Include request IDs for debugging","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Security (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-authentication","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Proper auth implementation (OAuth2/JWT)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-authorization","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Resource-level permissions (RBAC)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-rate-limiting","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Prevent abuse with rate limiting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-input-validation","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Validate and sanitize all input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-cors-config","type":"text","marks":[{"type":"code_inline"}]},{"text":" - CORS configuration with whitelists","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-https-only","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Enforce HTTPS for all traffic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sec-sensitive-data","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Protect passwords, tokens, PII","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Pagination & Filtering (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"page-cursor-based","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Cursor pagination for large datasets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"page-offset-based","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Offset pagination for simple cases","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"page-consistent-params","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Consistent parameter naming","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"page-metadata","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Include pagination metadata in responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"filter-query-params","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Filter via query parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sort-flexible","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Flexible sorting with ","type":"text"},{"text":"-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix for descending","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Versioning (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ver-url-path","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Version in URL path (/api/v1/)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ver-header-based","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Version via Accept header","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ver-backward-compatible","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Maintain backward compatibility","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ver-deprecation","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Deprecation strategy with Sunset header","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Response Format (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"resp-consistent-structure","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Consistent response envelope","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"resp-json-conventions","type":"text","marks":[{"type":"code_inline"}]},{"text":" - JSON naming conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"resp-partial-responses","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Field selection (sparse fieldsets)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"resp-compression","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Response compression (gzip/Brotli)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Documentation (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"doc-openapi","type":"text","marks":[{"type":"code_inline"}]},{"text":" - OpenAPI/Swagger specification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"doc-examples","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Request/response examples","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"doc-changelog","type":"text","marks":[{"type":"code_inline"}]},{"text":" - API changelog","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Essential Guidelines","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Resource Naming","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# ❌ Verbs in URLs\nGET /getUsers\nPOST /createUser\n\n# ✅ Nouns with HTTP methods\nGET /users # List users\nPOST /users # Create user\nGET /users/123 # Get user\nPUT /users/123 # Update user (full)\nPATCH /users/123 # Update user (partial)\nDELETE /users/123 # Delete user","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Error Response Format","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"The request contains invalid data\",\n \"details\": [\n {\n \"field\": \"email\",\n \"code\": \"INVALID_FORMAT\",\n \"message\": \"Please provide a valid email address\"\n }\n ],\n \"request_id\": \"req_abc123\"\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pagination","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"data\": [...],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 20,\n \"total_pages\": 10,\n \"total_count\": 195\n },\n \"links\": {\n \"first\": \"/users?page=1&per_page=20\",\n \"prev\": \"/users?page=1&per_page=20\",\n \"next\": \"/users?page=3&per_page=20\",\n \"last\": \"/users?page=10&per_page=20\"\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rate Limiting Headers","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"HTTP/1.1 200 OK\nX-RateLimit-Limit: 1000\nX-RateLimit-Remaining: 998\nX-RateLimit-Reset: 1640995200","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual rule files for detailed explanations:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"rules/rest-http-methods.md\nrules/error-consistent-format.md\nrules/page-cursor-based.md\nrules/sec-authentication.md\nrules/ver-url-path.md\nrules/doc-openapi.md","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"RESTful API Guidelines","type":"text","marks":[{"type":"link","attrs":{"href":"https://restfulapi.net","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Zalando RESTful API Guidelines","type":"text","marks":[{"type":"link","attrs":{"href":"https://zalando.github.io/restful-api-guidelines","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Microsoft API Guidelines","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/microsoft/api-guidelines","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Google API Design Guide","type":"text","marks":[{"type":"link","attrs":{"href":"https://cloud.google.com/apis/design","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenAPI Specification","type":"text","marks":[{"type":"link","attrs":{"href":"https://swagger.io/specification","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Full Compiled Document","type":"text"}]},{"type":"paragraph","content":[{"text":"For the complete guide with all rules expanded: ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]}]}]},"metadata":{"date":"2026-11-02","name":"api-design-patterns","author":"@skillopedia","source":{"stars":98,"repo_name":"agent-skills","origin_url":"https://github.com/AsyrafHussin/agent-skills/blob/HEAD/skills/api-design-patterns/SKILL.md","repo_owner":"AsyrafHussin","body_sha256":"9762b4a7c69a2578658ce432d6b4eb5e2f7bfc74a42cd5c190dd15c0072bd3c7","cluster_key":"8a6a3500c0eed32639c2a22e2e8c385c2d89381bc85bbd156a5efc273d8d8494","clean_bundle":{"format":"clean-skill-bundle-v1","source":"AsyrafHussin/agent-skills/skills/api-design-patterns/SKILL.md","text_bytes":400668,"source_commit":"1aa0ff717c10309226c9e678f00873976450fd76","skills_sh_rank":13434,"attachment_count":43,"text_attachments":43,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/api-design-patterns/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"software-engineering","metadata":{"author":"agent-skills","version":"2.0.0"},"import_tag":"skills-sh-top20k-2026-11","description":"RESTful API design, error handling, versioning, and best practices. Use when designing APIs, reviewing endpoints, implementing error responses, or setting up API structure. Triggers on \"design API\", \"review API\", \"REST best practices\", or \"API patterns\"."}},"renderedAt":1793665930111}

API Design Patterns RESTful API design principles for building consistent, developer-friendly APIs. Contains 38 rules across 7 categories covering resource design, error handling, security, pagination, versioning, response format, and documentation. Metadata - Version: 2.0.0 - Rule Count: 38 rules across 7 categories - License: MIT When to Apply Reference these guidelines when: - Designing new API endpoints - Reviewing existing API structure - Implementing error handling and validation - Setting up pagination, filtering, and sorting - Planning API versioning strategy - Configuring API securit…