diff --git a/README.md b/README.md index d3276c2..282e917 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,78 @@ -# Tech Documentation Standards +# Technical Documentation Standards -A comprehensive guide to building great software regardless of language and/or tools, primarily based on web standards and best practices. +> **Building great software through web standards and best practices** + +This repository contains comprehensive, in-depth guides for building modern software systems. Each guide is designed to take you from foundational concepts to senior architect-level understanding, with a strong focus on web standards (RFCs, ISOs, W3C specifications) and real-world implementation. ## Core Workflows -This documentation covers three core workflows for building modern applications: +### πŸ”Œ [REST APIs](./apis/) +Master the art of designing and implementing production-grade REST APIs. From versioning strategies rooted in HTTP specifications to advanced caching mechanisms following RFC 7234, these guides cover every aspect of building robust, scalable APIs. + +**Topics:** +- [Versioning](./apis/versioning.md) - Strategies for API evolution +- [Error Handling](./apis/error-handling.md) - Standardized error responses +- [Authentication](./apis/authentication.md) - Security and access control +- [Filtering](./apis/filtering.md) - Query patterns and data retrieval +- [Pagination](./apis/pagination.md) - Handling large datasets +- [Rate Limiting](./apis/rate-limiting.md) - Protecting your resources +- [Content Negotiation](./apis/content-negotiation.md) - Multiple data formats +- [Idempotency](./apis/idempotency.md) - Safe request retries +- [Caching](./apis/caching.md) - HTTP caching strategies +- [Observability](./apis/observability.md) - Logging, monitoring, and health checks + +### 🎨 [Frontend Development](./frontend/) +Build accessible, performant, and maintainable frontend applications using modern web standards. Learn how browsers work, how to optimize for Core Web Vitals, and how to create inclusive user experiences. -1. **[REST APIs](./apis.md)** - Best practices for designing and implementing RESTful APIs -2. **[Frontend](./frontend.md)** - Guidelines for building modern frontend applications -3. **[Libraries](./libraries.md)** - Standards for creating reusable libraries and packages +**Topics:** +- Project Structure & Architecture +- Performance Optimization +- Accessibility (WCAG, ARIA) +- State Management Patterns +- Routing & Navigation +- Forms & Validation +- API Integration +- Error Handling & Resilience +- Testing Strategies +- Security Best Practices +- Build & Deployment +- Developer Experience -## REST API Documentation +### πŸ“¦ [Libraries & Packages](./libraries/) +Create reusable, well-documented libraries that developers love to use. From package design to distribution, learn the patterns and practices that make great open-source projects. -The API documentation covers essential topics including: +**Topics:** +- Package Architecture +- API Design Principles +- Documentation Standards +- TypeScript & Type Safety +- Testing & Quality Assurance +- Versioning & Releases +- Performance Optimization +- Bundle Size Management +- Backwards Compatibility +- Security Practices +- Developer Experience +- Publishing & Distribution -- **Versioning** - Managing API versions and deprecation strategies -- **Error Handling** - Standardized error responses and status codes -- **Authentication** - Security and access control mechanisms -- **Filtering** - Query parameters for data filtering -- **Pagination** - Handling large datasets efficiently -- **Rate Limits** - Protecting API resources from abuse -- **Content Formats** - Supporting multiple data formats (JSON, XML, etc.) -- **Idempotency** - Ensuring safe request retries -- **Caching** - Optimizing performance with HTTP caching -- **Logging & Health Checks** - Monitoring and observability +## Philosophy + +These guides are written with several principles in mind: + +1. **Progressive Disclosure**: Start with the problem and build understanding step-by-step +2. **Standards-First**: Ground every recommendation in web standards, RFCs, and specifications +3. **Real-World Focus**: Practical examples from production systems +4. **Accessibility**: Written in plain English, approachable for junior developers +5. **Depth**: Detailed enough to give senior architect-level understanding ## Contributing -This is a living document. Contributions and improvements are welcome! +This is a living documentation project. Contributions, corrections, and improvements are welcome! Please ensure any additions: + +- Follow the established writing style (approachable, narrative, progressive) +- Reference relevant standards (RFCs, W3C specs, ISOs) +- Include practical, runnable examples +- Build understanding progressively ## License diff --git a/apis.md b/apis.md deleted file mode 100644 index a48252d..0000000 --- a/apis.md +++ /dev/null @@ -1,1165 +0,0 @@ -# REST API Best Practices - -A comprehensive guide to designing and implementing robust, scalable REST APIs based on web standards and industry best practices. - -## Table of Contents - -1. [Versioning](#versioning) -2. [Error Handling](#error-handling) -3. [Authentication](#authentication) -4. [Filtering](#filtering) -5. [Pagination](#pagination) -6. [Rate Limits](#rate-limits) -7. [Content Formats](#content-formats) -8. [Idempotency](#idempotency) -9. [Caching](#caching) -10. [Logging & Health Checks](#logging--health-checks) - ---- - -## Versioning - -API versioning ensures backward compatibility while allowing for evolution of your API. - -### Strategies - -**1. URL Versioning** (Recommended) -``` -GET /api/v1/users -GET /api/v2/users -``` -- **Pros**: Clear, explicit, easy to route -- **Cons**: URL changes with versions - -**2. Header Versioning** -``` -GET /api/users -Accept: application/vnd.myapi.v1+json -``` -- **Pros**: Clean URLs, follows content negotiation -- **Cons**: Less visible, harder to test - -**3. Query Parameter Versioning** -``` -GET /api/users?version=1 -``` -- **Pros**: Easy to implement -- **Cons**: Can be overlooked, mixed with other query params - -### Best Practices - -- Use semantic versioning (v1, v2, v3) -- Only increment major version for breaking changes -- Maintain at least one previous version -- Provide clear migration guides -- Set deprecation timelines (e.g., 12 months notice) -- Document version differences - -### Example Response with Version Info - -```json -{ - "api_version": "2.0", - "data": { - "users": [...] - } -} -``` - ---- - -## Error Handling - -Consistent error handling improves developer experience and debugging. - -### HTTP Status Codes - -Use appropriate status codes: - -**2xx Success** -- `200 OK` - Successful GET, PUT, PATCH, DELETE -- `201 Created` - Successful POST that creates a resource -- `204 No Content` - Successful request with no response body - -**4xx Client Errors** -- `400 Bad Request` - Invalid request syntax or validation error -- `401 Unauthorized` - Authentication required or failed -- `403 Forbidden` - Authenticated but not authorized -- `404 Not Found` - Resource doesn't exist -- `409 Conflict` - Request conflicts with current state -- `422 Unprocessable Entity` - Validation errors -- `429 Too Many Requests` - Rate limit exceeded - -**5xx Server Errors** -- `500 Internal Server Error` - Generic server error -- `502 Bad Gateway` - Invalid upstream response -- `503 Service Unavailable` - Service temporarily unavailable -- `504 Gateway Timeout` - Upstream timeout - -### Error Response Format - -Standardize error responses: - -```json -{ - "error": { - "code": "VALIDATION_ERROR", - "message": "Invalid request parameters", - "details": [ - { - "field": "email", - "message": "Invalid email format", - "code": "INVALID_EMAIL" - }, - { - "field": "age", - "message": "Must be at least 18", - "code": "MIN_VALUE" - } - ], - "request_id": "req_abc123", - "timestamp": "2024-01-15T10:30:00Z" - } -} -``` - -### Error Code Conventions - -Use consistent error codes: - -``` -RESOURCE_NOT_FOUND -AUTHENTICATION_REQUIRED -INSUFFICIENT_PERMISSIONS -VALIDATION_ERROR -RATE_LIMIT_EXCEEDED -INTERNAL_ERROR -SERVICE_UNAVAILABLE -``` - -### Best Practices - -- Always include a human-readable message -- Provide actionable error details -- Include request ID for tracking -- Don't expose sensitive information or stack traces -- Use consistent error structure across all endpoints -- Provide error code documentation - ---- - -## Authentication - -Secure your API with proper authentication mechanisms. - -### Methods - -**1. API Keys** (Simple, for server-to-server) -``` -Authorization: ApiKey YOUR_API_KEY -X-API-Key: YOUR_API_KEY -``` - -**2. Bearer Tokens / JWT** (Recommended) -``` -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -**3. OAuth 2.0** (For delegated access) -``` -Authorization: Bearer ACCESS_TOKEN -``` - -**4. Basic Auth** (Legacy, use with HTTPS only) -``` -Authorization: Basic base64(username:password) -``` - -### Best Practices - -- Always use HTTPS/TLS in production -- Implement token expiration and refresh mechanisms -- Use short-lived access tokens (15-60 minutes) -- Store tokens securely (never in URLs or logs) -- Implement token revocation -- Use scopes/permissions for fine-grained access control - -### Example JWT Payload - -```json -{ - "sub": "user_123", - "iss": "https://api.example.com", - "aud": "https://api.example.com", - "exp": 1705324800, - "iat": 1705321200, - "scopes": ["read:users", "write:users"] -} -``` - -### Authentication Errors - -```json -{ - "error": { - "code": "AUTHENTICATION_REQUIRED", - "message": "Valid authentication credentials required", - "details": "Missing or invalid Authorization header" - } -} -``` - ---- - -## Filtering - -Enable clients to query specific subsets of data. - -### Query Parameters - -**Basic Filtering** -``` -GET /api/users?status=active -GET /api/users?role=admin&status=active -``` - -**Comparison Operators** -``` -GET /api/products?price[gte]=100&price[lte]=500 -GET /api/users?created_at[gt]=2024-01-01 -``` - -**Multiple Values (OR logic)** -``` -GET /api/users?status=active,pending -GET /api/users?id=1,2,3 -``` - -**Pattern Matching** -``` -GET /api/users?name[like]=John* -GET /api/users?email[contains]=@example.com -``` - -### Advanced Filtering - -**Field Selection (Sparse Fieldsets)** -``` -GET /api/users?fields=id,name,email -``` - -**Nested Filtering** -``` -GET /api/orders?customer.country=US -GET /api/posts?author.verified=true -``` - -### Filter Operators - -| Operator | Description | Example | -|----------|-------------|---------| -| `eq` | Equal | `status[eq]=active` | -| `ne` | Not equal | `status[ne]=deleted` | -| `gt` | Greater than | `age[gt]=18` | -| `gte` | Greater than or equal | `price[gte]=100` | -| `lt` | Less than | `age[lt]=65` | -| `lte` | Less than or equal | `price[lte]=1000` | -| `in` | In array | `status[in]=active,pending` | -| `nin` | Not in array | `role[nin]=guest,banned` | -| `like` | Pattern match | `name[like]=John*` | -| `contains` | Contains substring | `email[contains]=@gmail` | - -### Best Practices - -- Document all filterable fields -- Validate filter parameters -- Set reasonable defaults -- Limit complexity to prevent abuse -- Support case-insensitive filtering where appropriate -- Return empty results (not errors) for valid filters with no matches - ---- - -## Pagination - -Handle large datasets efficiently with pagination. - -### Offset-Based Pagination - -**Request** -``` -GET /api/users?limit=20&offset=40 -``` - -**Response** -```json -{ - "data": [...], - "pagination": { - "limit": 20, - "offset": 40, - "total": 150, - "total_pages": 8, - "current_page": 3 - }, - "links": { - "first": "/api/users?limit=20&offset=0", - "prev": "/api/users?limit=20&offset=20", - "next": "/api/users?limit=20&offset=60", - "last": "/api/users?limit=20&offset=140" - } -} -``` - -**Pros**: Simple, can jump to any page -**Cons**: Performance degrades with high offsets, inconsistent results if data changes - -### Cursor-Based Pagination (Recommended for large datasets) - -**Request** -``` -GET /api/users?limit=20&cursor=eyJpZCI6MTAwfQ -``` - -**Response** -```json -{ - "data": [...], - "pagination": { - "limit": 20, - "next_cursor": "eyJpZCI6MTIwfQ", - "prev_cursor": "eyJpZCI6ODAfQ", - "has_more": true - }, - "links": { - "next": "/api/users?limit=20&cursor=eyJpZCI6MTIwfQ", - "prev": "/api/users?limit=20&cursor=eyJpZCI6ODAfQ" - } -} -``` - -**Pros**: Consistent results, efficient for large datasets -**Cons**: Can't jump to arbitrary pages - -### Page-Based Pagination - -**Request** -``` -GET /api/users?page=3&per_page=20 -``` - -**Response** -```json -{ - "data": [...], - "pagination": { - "page": 3, - "per_page": 20, - "total": 150, - "total_pages": 8 - } -} -``` - -### HTTP Headers for Pagination - -``` -Link: ; rel="next", - ; rel="prev", - ; rel="first", - ; rel="last" -X-Total-Count: 150 -X-Page: 2 -X-Per-Page: 20 -``` - -### Best Practices - -- Set a default page size (e.g., 20-50 items) -- Set a maximum page size (e.g., 100 items) -- Always include pagination metadata -- Provide navigation links -- Use cursor-based pagination for real-time data -- Document pagination behavior -- Consider performance implications of total counts - ---- - -## Rate Limits - -Protect your API from abuse and ensure fair usage. - -### Implementation - -**Fixed Window** -- 1000 requests per hour per user -- Simple but can allow bursts at window boundaries - -**Sliding Window** -- 1000 requests per rolling 60-minute window -- More accurate, prevents boundary abuse - -**Token Bucket** -- Allows controlled bursts -- Refills at a steady rate - -### HTTP Headers - -Return rate limit information in response headers: - -``` -X-RateLimit-Limit: 1000 -X-RateLimit-Remaining: 237 -X-RateLimit-Reset: 1705324800 -X-RateLimit-Reset-After: 3600 -Retry-After: 3600 -``` - -### Rate Limit Exceeded Response - -**Status**: `429 Too Many Requests` - -```json -{ - "error": { - "code": "RATE_LIMIT_EXCEEDED", - "message": "API rate limit exceeded", - "details": { - "limit": 1000, - "window": "1 hour", - "reset_at": "2024-01-15T11:00:00Z", - "retry_after": 3600 - } - } -} -``` - -### Rate Limit Tiers - -Different limits for different user types: - -| Tier | Requests/Hour | Burst | -|------|---------------|-------| -| Anonymous | 100 | 10 | -| Authenticated | 1,000 | 50 | -| Premium | 10,000 | 200 | -| Enterprise | Custom | Custom | - -### Best Practices - -- Document rate limits clearly -- Return limit info in all responses -- Use `429` status code when exceeded -- Provide `Retry-After` header -- Consider different limits for different endpoints -- Allow rate limit increases for verified users -- Implement gradual backoff for repeated violations -- Monitor and alert on rate limit abuse patterns - ---- - -## Content Formats - -Support multiple content formats based on client needs. - -### Content Negotiation - -**Request** -``` -GET /api/users -Accept: application/json -``` - -**Response** -``` -Content-Type: application/json; charset=utf-8 - -{ - "users": [...] -} -``` - -### Supported Formats - -**JSON** (Default, Recommended) -``` -Accept: application/json -Content-Type: application/json -``` - -**XML** -``` -Accept: application/xml -Content-Type: application/xml -``` - -**CSV** (For exports) -``` -Accept: text/csv -Content-Type: text/csv -``` - -**Protocol Buffers** (For high-performance scenarios) -``` -Accept: application/x-protobuf -Content-Type: application/x-protobuf -``` - -### JSON Best Practices - -**Use camelCase or snake_case consistently** -```json -{ - "userId": 123, - "firstName": "John" -} -``` - -or - -```json -{ - "user_id": 123, - "first_name": "John" -} -``` - -**Use ISO 8601 for dates** -```json -{ - "created_at": "2024-01-15T10:30:00Z", - "updated_at": "2024-01-15T14:45:00+00:00" -} -``` - -**Use null for missing values** -```json -{ - "name": "John", - "middle_name": null, - "age": 30 -} -``` - -**Envelope responses consistently** -```json -{ - "data": {...}, - "meta": { - "timestamp": "2024-01-15T10:30:00Z", - "version": "2.0" - } -} -``` - -### Content Compression - -Support compression for large responses: - -``` -Accept-Encoding: gzip, deflate, br -Content-Encoding: gzip -``` - -### Unsupported Format Response - -**Status**: `406 Not Acceptable` - -```json -{ - "error": { - "code": "UNSUPPORTED_MEDIA_TYPE", - "message": "Requested format not supported", - "supported_formats": ["application/json", "application/xml"] - } -} -``` - -### Best Practices - -- Default to JSON for modern APIs -- Support content negotiation via Accept header -- Use UTF-8 encoding -- Enable compression for responses > 1KB -- Version your content types if format changes -- Document supported formats clearly -- Validate Content-Type on requests - ---- - -## Idempotency - -Ensure safe request retries and prevent duplicate operations. - -### Idempotent Methods - -**Naturally Idempotent** -- `GET` - Safe to retry, no side effects -- `PUT` - Replaces resource, same result -- `DELETE` - Deletes resource, same result -- `HEAD` - Safe to retry, no side effects -- `OPTIONS` - Safe to retry, no side effects - -**Not Idempotent by Default** -- `POST` - Creates new resource each time -- `PATCH` - May produce different results - -### Idempotency Keys - -For non-idempotent operations (especially POST), use idempotency keys: - -**Request** -``` -POST /api/payments -Idempotency-Key: key_abc123xyz789 -Content-Type: application/json - -{ - "amount": 1000, - "currency": "USD", - "customer_id": "cust_123" -} -``` - -**First Response** (Creates resource) -``` -HTTP/1.1 201 Created -Content-Type: application/json - -{ - "id": "pay_456", - "status": "succeeded", - "amount": 1000 -} -``` - -**Retry with Same Key** (Returns same result) -``` -HTTP/1.1 200 OK -Content-Type: application/json - -{ - "id": "pay_456", - "status": "succeeded", - "amount": 1000 -} -``` - -### Implementation Guidelines - -**Key Requirements** -- Client-generated unique identifier (UUID recommended) -- Store key-response mapping for 24 hours minimum -- Return 409 Conflict if same key used with different payload -- Clean up old keys periodically - -**Validation** -```json -{ - "error": { - "code": "IDEMPOTENCY_KEY_MISMATCH", - "message": "Request parameters differ from original request with this idempotency key", - "original_request_id": "req_789" - } -} -``` - -### Best Practices - -- Use UUIDs or cryptographically random strings -- Set appropriate expiration (24-72 hours) -- Store minimal data (request hash + response) -- Return original response status code -- Document idempotency behavior -- Handle concurrent requests with same key gracefully -- Use for financial transactions, order creation, etc. - -### Example Idempotent POST - -```javascript -// Client implementation -async function createPayment(paymentData) { - const idempotencyKey = generateUUID(); - - return fetch('/api/payments', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Idempotency-Key': idempotencyKey - }, - body: JSON.stringify(paymentData) - }); -} -``` - ---- - -## Caching - -Optimize performance and reduce server load with effective caching strategies. - -### HTTP Cache Headers - -**Cache-Control** -``` -Cache-Control: public, max-age=3600 -Cache-Control: private, max-age=300 -Cache-Control: no-cache -Cache-Control: no-store -``` - -**Directives** -- `public` - Cacheable by any cache -- `private` - Cacheable by client only -- `no-cache` - Revalidate before use -- `no-store` - Don't cache at all -- `max-age=N` - Cache for N seconds -- `s-maxage=N` - CDN/shared cache override -- `must-revalidate` - Strict revalidation when stale - -### ETags (Entity Tags) - -**First Request** -``` -GET /api/users/123 -``` - -**Response** -``` -HTTP/1.1 200 OK -ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" -Cache-Control: max-age=0, must-revalidate - -{ - "id": 123, - "name": "John Doe" -} -``` - -**Conditional Request** -``` -GET /api/users/123 -If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" -``` - -**Not Modified Response** -``` -HTTP/1.1 304 Not Modified -ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" -``` - -### Last-Modified - -**Response** -``` -HTTP/1.1 200 OK -Last-Modified: Mon, 15 Jan 2024 10:30:00 GMT -Cache-Control: max-age=3600 -``` - -**Conditional Request** -``` -GET /api/users/123 -If-Modified-Since: Mon, 15 Jan 2024 10:30:00 GMT -``` - -### Caching Strategies by Endpoint Type - -**Static/Rarely Changed Data** -``` -Cache-Control: public, max-age=86400, immutable -``` - -**User-Specific Data** -``` -Cache-Control: private, max-age=300 -``` - -**Real-Time Data** -``` -Cache-Control: no-cache, must-revalidate -``` - -**Sensitive Data** -``` -Cache-Control: no-store, private -``` - -**API Responses with Frequent Updates** -``` -Cache-Control: public, max-age=60, stale-while-revalidate=300 -``` - -### Vary Header - -Indicate which request headers affect caching: - -``` -Vary: Accept, Accept-Encoding, Authorization -``` - -### Cache Invalidation - -**Time-Based** (TTL) -- Simplest approach -- Use `max-age` directive - -**Event-Based** -- Invalidate on resource changes -- Use cache keys with version/timestamp - -**Purge API** -- Explicit cache clearing -- For critical updates - -### Best Practices - -- Use ETags for frequently accessed resources -- Set appropriate `max-age` based on data volatility -- Use `private` for personalized content -- Use `public` for shared resources -- Combine with CDN for global caching -- Document caching behavior -- Monitor cache hit rates -- Use `Vary` header appropriately -- Consider stale-while-revalidate for better UX -- Never cache sensitive data (passwords, tokens) - -### Example Caching Strategy - -```javascript -// Server-side caching logic -app.get('/api/products/:id', (req, res) => { - const product = getProduct(req.params.id); - const etag = generateETag(product); - - // Check if client has current version - if (req.headers['if-none-match'] === etag) { - return res.status(304).end(); - } - - res.set({ - 'Cache-Control': 'public, max-age=3600', - 'ETag': etag, - 'Vary': 'Accept-Encoding' - }); - - res.json(product); -}); -``` - ---- - -## Logging & Health Checks - -Ensure observability, monitoring, and reliability of your API. - -### Logging - -**What to Log** - -**Request Logs** -```json -{ - "timestamp": "2024-01-15T10:30:00Z", - "request_id": "req_abc123", - "method": "POST", - "path": "/api/users", - "status": 201, - "duration_ms": 45, - "ip": "192.168.1.100", - "user_agent": "Mozilla/5.0...", - "user_id": "user_456" -} -``` - -**Error Logs** -```json -{ - "timestamp": "2024-01-15T10:30:00Z", - "level": "error", - "request_id": "req_abc123", - "error": { - "type": "DatabaseConnectionError", - "message": "Connection timeout", - "stack": "Error: Connection timeout\n at ...", - "code": "ECONNREFUSED" - }, - "context": { - "user_id": "user_456", - "endpoint": "/api/users" - } -} -``` - -**Security Logs** -```json -{ - "timestamp": "2024-01-15T10:30:00Z", - "event": "authentication_failed", - "ip": "192.168.1.100", - "user_id": null, - "reason": "invalid_credentials", - "attempt_count": 3 -} -``` - -### Log Levels - -- `DEBUG` - Detailed diagnostic information -- `INFO` - General informational messages -- `WARN` - Warning messages, potential issues -- `ERROR` - Error events, still functioning -- `FATAL` - Severe errors, application crash - -### Structured Logging - -Use JSON format for machine-readable logs: - -```json -{ - "timestamp": "2024-01-15T10:30:00.123Z", - "level": "info", - "service": "api-gateway", - "environment": "production", - "request_id": "req_abc123", - "message": "User created successfully", - "data": { - "user_id": "user_789", - "email": "user@example.com" - } -} -``` - -### What NOT to Log - -- Passwords or authentication credentials -- Credit card numbers or PII -- API keys or secrets -- Full request/response bodies with sensitive data - -### Request IDs - -Generate unique request IDs for tracing: - -``` -X-Request-ID: req_abc123xyz789 -``` - -Include in all log entries and error responses. - -### Health Checks - -**Basic Health Check** - -``` -GET /health -``` - -**Response** -```json -{ - "status": "healthy", - "timestamp": "2024-01-15T10:30:00Z", - "version": "2.0.0", - "uptime": 86400 -} -``` - -**Detailed Health Check** - -``` -GET /health/detailed -``` - -**Response** -```json -{ - "status": "healthy", - "timestamp": "2024-01-15T10:30:00Z", - "version": "2.0.0", - "uptime": 86400, - "checks": { - "database": { - "status": "healthy", - "response_time_ms": 12, - "details": "PostgreSQL 14.0" - }, - "cache": { - "status": "healthy", - "response_time_ms": 3, - "details": "Redis 7.0" - }, - "external_api": { - "status": "degraded", - "response_time_ms": 850, - "details": "High latency" - } - }, - "metrics": { - "requests_per_second": 145, - "error_rate": 0.02, - "avg_response_time_ms": 78 - } -} -``` - -### Health Check Status Codes - -- `200 OK` - Healthy -- `503 Service Unavailable` - Unhealthy -- `429 Too Many Requests` - Health check rate limited - -### Readiness vs Liveness - -**Liveness Probe** (`/health/live`) -- Is the application running? -- Used to restart crashed containers - -**Readiness Probe** (`/health/ready`) -- Is the application ready to serve traffic? -- Used to remove from load balancer during startup/shutdown - -``` -GET /health/ready -``` - -**Response (Not Ready)** -```json -{ - "status": "not_ready", - "reason": "database_connection_pending", - "checks": { - "database": { - "status": "initializing" - } - } -} -``` - -### Monitoring Metrics - -**Key Metrics to Track** - -- Request rate (requests/second) -- Error rate (4xx, 5xx) -- Response time (p50, p95, p99) -- Availability (uptime %) -- Throughput -- Active connections -- Queue depth - -**Metrics Endpoint** - -``` -GET /metrics -``` - -**Response (Prometheus format)** -``` -# HELP http_requests_total Total HTTP requests -# TYPE http_requests_total counter -http_requests_total{method="GET",status="200"} 1234567 - -# HELP http_request_duration_seconds HTTP request latency -# TYPE http_request_duration_seconds histogram -http_request_duration_seconds_bucket{le="0.1"} 10000 -http_request_duration_seconds_bucket{le="0.5"} 25000 -``` - -### Best Practices - -**Logging** -- Use structured logging (JSON) -- Include request IDs in all logs -- Set appropriate log levels -- Rotate logs to prevent disk space issues -- Centralize logs for distributed systems -- Sanitize sensitive data before logging -- Log at application boundaries (requests, database calls, external APIs) - -**Health Checks** -- Keep health checks lightweight -- Don't expose sensitive information -- Implement both liveness and readiness probes -- Include dependency health (database, cache, external APIs) -- Set appropriate timeouts -- Cache health check results briefly to prevent overhead -- Monitor health check failures - -**Monitoring** -- Set up alerts for key metrics -- Track SLIs (Service Level Indicators) -- Define SLOs (Service Level Objectives) -- Use distributed tracing for complex systems -- Monitor business metrics alongside technical metrics -- Create dashboards for real-time visibility - -### Example Implementation - -```javascript -// Express.js health check middleware -app.get('/health', async (req, res) => { - const health = { - status: 'healthy', - timestamp: new Date().toISOString(), - version: process.env.APP_VERSION, - uptime: process.uptime() - }; - - try { - // Quick dependency checks - await db.ping(); - await cache.ping(); - - res.status(200).json(health); - } catch (error) { - health.status = 'unhealthy'; - health.error = error.message; - res.status(503).json(health); - } -}); - -// Request logging middleware -app.use((req, res, next) => { - const requestId = req.headers['x-request-id'] || generateUUID(); - req.requestId = requestId; - res.setHeader('X-Request-ID', requestId); - - const startTime = Date.now(); - - res.on('finish', () => { - logger.info({ - timestamp: new Date().toISOString(), - request_id: requestId, - method: req.method, - path: req.path, - status: res.statusCode, - duration_ms: Date.now() - startTime, - ip: req.ip, - user_agent: req.headers['user-agent'] - }); - }); - - next(); -}); -``` - ---- - -## Summary - -Following these REST API best practices will help you build robust, scalable, and developer-friendly APIs: - -1. **Version your API** to allow evolution while maintaining backward compatibility -2. **Handle errors consistently** with proper status codes and structured responses -3. **Secure with authentication** using modern standards like JWT and OAuth 2.0 -4. **Enable filtering** to let clients query exactly what they need -5. **Implement pagination** for efficient handling of large datasets -6. **Apply rate limits** to protect your API from abuse -7. **Support multiple content formats** with proper content negotiation -8. **Use idempotency keys** for safe retries of critical operations -9. **Implement caching** to improve performance and reduce load -10. **Log comprehensively and provide health checks** for observability and reliability - -These standards create a foundation for APIs that are reliable, performant, and pleasant to use. diff --git a/apis/README.md b/apis/README.md new file mode 100644 index 0000000..a150123 --- /dev/null +++ b/apis/README.md @@ -0,0 +1,79 @@ +# REST API Best Practices + +> **Designing APIs that stand the test of time** + +Modern web applications live and die by their APIs. Whether you're building a mobile app, connecting microservices, or creating a platform for third-party developers, your API is the contract that defines how systems communicate. + +This section covers everything you need to know to design, implement, and maintain production-grade REST APIs based on web standards and battle-tested patterns. + +## What You'll Learn + +Each guide in this section takes you from fundamental concepts to advanced implementation details: + +### Core Topics + +1. **[Versioning](./versioning.md)** - How to evolve your API without breaking existing clients +2. **[Error Handling](./error-handling.md)** - Standardized approaches to communicating failures +3. **[Authentication](./authentication.md)** - Securing your API with modern authentication methods +4. **[Filtering](./filtering.md)** - Enabling clients to query exactly the data they need +5. **[Pagination](./pagination.md)** - Efficiently handling large datasets +6. **[Rate Limiting](./rate-limiting.md)** - Protecting your infrastructure from abuse +7. **[Content Negotiation](./content-negotiation.md)** - Supporting multiple data formats +8. **[Idempotency](./idempotency.md)** - Making operations safe to retry +9. **[Caching](./caching.md)** - Leveraging HTTP caching for performance +10. **[Observability](./observability.md)** - Logging, monitoring, and debugging in production + +## REST Fundamentals + +REST (Representational State Transfer) isn't just a style guideβ€”it's an architectural pattern grounded in the HTTP protocol specification (RFC 7231). Understanding REST means understanding how the web was designed to work. + +### The Six Constraints + +Roy Fielding's original dissertation defined REST with six architectural constraints: + +1. **Client-Server** - Separation of concerns between UI and data storage +2. **Stateless** - Each request contains all information needed to understand it +3. **Cacheable** - Responses must define themselves as cacheable or not +4. **Uniform Interface** - Consistent way to interact with resources +5. **Layered System** - Client can't tell if connected directly to server +6. **Code on Demand** (optional) - Servers can extend client functionality + +## Getting Started + +If you're new to API design, we recommend starting with these guides in order: + +1. Start with **[Error Handling](./error-handling.md)** to understand how to communicate problems +2. Move to **[Authentication](./authentication.md)** to learn about securing your API +3. Then **[Versioning](./versioning.md)** to plan for evolution +4. Finally **[Caching](./caching.md)** to make it fast + +For specific challenges: +- **Building a search API?** β†’ Read [Filtering](./filtering.md) and [Pagination](./pagination.md) +- **Payment or transaction API?** β†’ Focus on [Idempotency](./idempotency.md) +- **Public API with many users?** β†’ Start with [Rate Limiting](./rate-limiting.md) +- **Need to debug issues?** β†’ Jump to [Observability](./observability.md) + +## Standards and Specifications + +These guides reference and build upon established web standards: + +- **HTTP/1.1** - RFC 7230-7235 (Protocol fundamentals) +- **HTTP/2** - RFC 7540 (Performance improvements) +- **HTTP/3** - RFC 9114 (QUIC-based HTTP) +- **URI** - RFC 3986 (Uniform Resource Identifiers) +- **JSON** - RFC 8259 (JavaScript Object Notation) +- **OAuth 2.0** - RFC 6749 (Authorization framework) +- **JWT** - RFC 7519 (JSON Web Tokens) +- **Problem Details** - RFC 7807 (HTTP API error responses) + +## Beyond This Guide + +API design is a vast topic. After mastering these fundamentals, consider exploring: + +- **GraphQL** for query flexibility +- **gRPC** for high-performance RPC +- **WebSockets** for real-time bidirectional communication +- **Server-Sent Events** for server-push updates +- **Webhooks** for event-driven integrations + +Each has its place, but REST remains the foundation of modern web APIs. diff --git a/apis/error-handling.md b/apis/error-handling.md new file mode 100644 index 0000000..a8abf3a --- /dev/null +++ b/apis/error-handling.md @@ -0,0 +1,1112 @@ +# Error Handling: When Things Go Wrong + +> **Errors are not failures of your APIβ€”they're conversations with your users about what went wrong and how to fix it.** + +It's 2 AM. A payment just failed. Your user's card was declined, but your API returned a generic "500 Internal Server Error." The mobile app shows "Something went wrong. Try again later." The user abandons their cart. Your company loses the sale. The developer who integrated your API gets blamed. + +This happens thousands of times a day across the web, and it's almost always preventable. The difference between a good API and a great one often comes down to how it handles errors. + +## The Real Cost of Bad Error Handling + +Let's be honest about what's at stake: + +**For Your Users**: +- Frustration when they don't know what went wrong +- Lost time trying the same failing action repeatedly +- Abandoned tasks because the error message said "contact support" + +**For Developers Using Your API**: +- Hours debugging cryptic error messages +- Support tickets asking "what does error code 42 mean?" +- Loss of trust in your API's reliability + +**For Your Business**: +- Increased support costs +- Reduced API adoption +- Bad reputation in developer communities + +Good error handling isn't just nice to haveβ€”it's essential infrastructure. + +## The Foundation: HTTP Status Codes + +Before we dive into error response formats, we need to understand the HTTP status code system. It's defined in RFC 7231 and it's more sophisticated than most people realize. + +### The Five Categories + +HTTP status codes use a simple pattern: the first digit indicates the category. + +**1xx: Informational** (100-199) +- The request was received and is being processed +- Rarely used in REST APIs +- Example: `100 Continue` + +**2xx: Success** (200-299) +- The request succeeded +- Different codes indicate different types of success +- Examples: `200 OK`, `201 Created`, `204 No Content` + +**3xx: Redirection** (300-399) +- Further action needed to complete the request +- Usually handled automatically by HTTP clients +- Examples: `301 Moved Permanently`, `304 Not Modified` + +**4xx: Client Errors** (400-499) +- The request contains an error (bad syntax, invalid data, etc.) +- **The client should not retry without changes** +- Examples: `400 Bad Request`, `404 Not Found`, `429 Too Many Requests` + +**5xx: Server Errors** (500-599) +- The server failed to fulfill a valid request +- **The client might retry and succeed** +- Examples: `500 Internal Server Error`, `503 Service Unavailable` + +This distinction between 4xx and 5xx is critical: it tells the client whether retrying makes sense. + +### Choosing the Right Status Code + +Here's how to choose status codes for common scenarios: + +#### Success Scenarios + +**`200 OK`** - The workhorse of HTTP +```javascript +// Successful GET, PUT, PATCH, or DELETE +GET /api/users/123 + +HTTP/1.1 200 OK +{ + "id": "123", + "name": "Alice" +} +``` +Use when: The request succeeded and you're returning data. + +**`201 Created`** - Resource creation confirmed +```javascript +// Successful POST that creates a new resource +POST /api/users + +HTTP/1.1 201 Created +Location: /api/users/124 +{ + "id": "124", + "name": "Bob" +} +``` +Use when: You created a new resource. Always include a `Location` header. + +**`204 No Content`** - Success with nothing to say +```javascript +// Successful DELETE +DELETE /api/users/123 + +HTTP/1.1 204 No Content +``` +Use when: The request succeeded but there's no data to return. + +**`202 Accepted`** - Request queued +```javascript +// Async operation started +POST /api/reports/generate + +HTTP/1.1 202 Accepted +{ + "job_id": "job_789", + "status": "pending", + "status_url": "/api/jobs/job_789" +} +``` +Use when: The request was accepted but will be processed later. + +#### Client Error Scenarios + +**`400 Bad Request`** - Generic client error +```javascript +// Malformed JSON +POST /api/users +{ + "name": "Alice" + "email": "alice@example.com" // Missing comma! +} + +HTTP/1.1 400 Bad Request +{ + "error": { + "type": "invalid_request", + "message": "Request body contains invalid JSON", + "details": "Expected comma at line 2, column 18" + } +} +``` +Use when: The request is syntactically invalid. + +**`401 Unauthorized`** - Not authenticated +```javascript +// Missing or invalid authentication +GET /api/users/me + +HTTP/1.1 401 Unauthorized +WWW-Authenticate: ****** realm="API" +{ + "error": { + "type": "authentication_required", + "message": "Valid authentication credentials required", + "details": "Include a valid ******in the Authorization header" + } +} +``` +Use when: The user hasn't provided credentials or they're invalid. + +Note the naming confusion: Despite its name, `401` means "not authenticated," not "not authorized." Authentication ("who are you?") comes before authorization ("what can you do?"). + +**`403 Forbidden`** - Not authorized +```javascript +// Authenticated but lacks permission +DELETE /api/users/123 + +HTTP/1.1 403 Forbidden +{ + "error": { + "type": "insufficient_permissions", + "message": "You don't have permission to delete this user", + "required_role": "admin", + "your_role": "user" + } +} +``` +Use when: The user is authenticated but doesn't have permission. + +**`404 Not Found`** - Resource doesn't exist +```javascript +GET /api/users/999 + +HTTP/1.1 404 Not Found +{ + "error": { + "type": "resource_not_found", + "message": "User not found", + "resource_type": "user", + "resource_id": "999" + } +} +``` +Use when: The requested resource doesn't exist. + +**`409 Conflict`** - Request conflicts with current state +```javascript +// Trying to create a user with an existing email +POST /api/users +{ + "email": "alice@example.com", + "name": "Alice" +} + +HTTP/1.1 409 Conflict +{ + "error": { + "type": "duplicate_resource", + "message": "A user with this email already exists", + "conflicting_field": "email", + "conflicting_value": "alice@example.com" + } +} +``` +Use when: The request is valid but conflicts with the current state. + +**`422 Unprocessable Entity`** - Validation failed +```javascript +// Valid JSON, but invalid data +POST /api/users +{ + "email": "not-an-email", + "age": -5 +} + +HTTP/1.1 422 Unprocessable Entity +{ + "error": { + "type": "validation_error", + "message": "Request validation failed", + "errors": [ + { + "field": "email", + "message": "Must be a valid email address", + "value": "not-an-email" + }, + { + "field": "age", + "message": "Must be a positive number", + "value": -5 + } + ] + } +} +``` +Use when: The request is syntactically valid but semantically incorrect. + +**`429 Too Many Requests`** - Rate limit exceeded +```javascript +// Too many requests from this client +GET /api/users + +HTTP/1.1 429 Too Many Requests +Retry-After: 60 +X-RateLimit-Limit: 1000 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1704729600 + +{ + "error": { + "type": "rate_limit_exceeded", + "message": "API rate limit exceeded", + "limit": 1000, + "window": "1 hour", + "retry_after": 60 + } +} +``` +Use when: The client has exceeded rate limits. + +#### Server Error Scenarios + +**`500 Internal Server Error`** - Something went wrong +```javascript +// Database connection failed +GET /api/users/123 + +HTTP/1.1 500 Internal Server Error +{ + "error": { + "type": "internal_error", + "message": "An internal error occurred", + "request_id": "req_abc123", + "timestamp": "2024-01-08T14:30:00Z" + } +} +``` +Use when: Your server encountered an unexpected condition. + +**Critical**: Never expose internal details (stack traces, database errors) in production. + +**`502 Bad Gateway`** - Upstream service failed +```javascript +// Payment provider returned an error +POST /api/payments + +HTTP/1.1 502 Bad Gateway +{ + "error": { + "type": "upstream_error", + "message": "Payment provider is unavailable", + "request_id": "req_abc123" + } +} +``` +Use when: A service you depend on failed. + +**`503 Service Unavailable`** - Temporary outage +```javascript +// Server is overloaded +GET /api/users + +HTTP/1.1 503 Service Unavailable +Retry-After: 120 +{ + "error": { + "type": "service_unavailable", + "message": "Service temporarily unavailable", + "retry_after": 120 + } +} +``` +Use when: The service is temporarily unavailable but will recover. + +**`504 Gateway Timeout`** - Upstream didn't respond +```javascript +// External API took too long +GET /api/external-data + +HTTP/1.1 504 Gateway Timeout +{ + "error": { + "type": "timeout", + "message": "Request to upstream service timed out", + "timeout": 30 + } +} +``` +Use when: A dependency didn't respond in time. + +## Error Response Format: RFC 7807 + +Now that we understand status codes, let's talk about the response body. There's an RFC for this: RFC 7807, "Problem Details for HTTP APIs." + +### The Standard Format + +RFC 7807 defines a JSON (or XML) format for error responses: + +```javascript +{ + "type": "https://api.example.com/errors/insufficient-credit", + "title": "Insufficient credit", + "status": 403, + "detail": "Your account has only $50 but this operation requires $100", + "instance": "/api/payments/txn_123", + "balance": 50, + "required": 100, + "account_id": "acc_456" +} +``` + +Let's break down each field: + +**`type`** (string, required) +- A URI that identifies the error type +- Should be a permalink to human-readable documentation +- Defaults to "about:blank" if omitted + +**`title`** (string, required) +- A short, human-readable summary +- Should be the same for all instances of this error type +- Think of it as the error "name" + +**`status`** (number, optional but recommended) +- The HTTP status code +- Duplicates the response status for convenience + +**`detail`** (string, optional) +- A human-readable explanation specific to this occurrence +- Can include instance-specific information + +**`instance`** (string, optional) +- A URI reference to this specific error occurrence +- Useful for debugging and support + +**Additional Fields** (optional) +- You can add custom fields specific to the error +- In the example above: `balance`, `required`, `account_id` + +### Practical Implementation + +Here's how to implement RFC 7807 in practice: + +```javascript +class ApiError extends Error { + constructor({ + type, + title, + status, + detail, + instance, + ...extensions + }) { + super(detail || title); + this.type = type || 'about:blank'; + this.title = title; + this.status = status; + this.detail = detail; + this.instance = instance; + this.extensions = extensions; + } + + toJSON() { + return { + type: this.type, + title: this.title, + status: this.status, + detail: this.detail, + instance: this.instance, + ...this.extensions + }; + } +} + +// Usage +app.post('/api/payments', async (req, res) => { + try { + const account = await getAccount(req.user.id); + const amount = req.body.amount; + + if (account.balance < amount) { + throw new ApiError({ + type: 'https://api.example.com/errors/insufficient-credit', + title: 'Insufficient credit', + status: 403, + detail: `Your account has $${account.balance} but this operation requires $${amount}`, + instance: `/api/payments/${req.id}`, + balance: account.balance, + required: amount, + account_id: account.id + }); + } + + // Process payment... + } catch (error) { + if (error instanceof ApiError) { + res.status(error.status).json(error.toJSON()); + } else { + // Handle unexpected errors + res.status(500).json({ + type: 'about:blank', + title: 'Internal Server Error', + status: 500, + detail: 'An unexpected error occurred', + instance: `/api/payments/${req.id}` + }); + } + } +}); +``` + +## Validation Errors: Going Deeper + +Validation errors deserve special attention because they're so common. When multiple fields fail validation, you need a format that communicates all the problems at once. + +### The Field-Level Error Pattern + +```javascript +HTTP/1.1 422 Unprocessable Entity +Content-Type: application/problem+json + +{ + "type": "https://api.example.com/errors/validation-error", + "title": "Validation Error", + "status": 422, + "detail": "The request contains invalid fields", + "instance": "/api/users", + "errors": [ + { + "field": "email", + "code": "invalid_format", + "message": "Must be a valid email address", + "value": "not-an-email" + }, + { + "field": "password", + "code": "too_short", + "message": "Must be at least 8 characters", + "value": "***", + "min_length": 8, + "actual_length": 3 + }, + { + "field": "age", + "code": "out_of_range", + "message": "Must be between 18 and 120", + "value": 15, + "min": 18, + "max": 120 + } + ] +} +``` + +Each error object includes: +- **field**: Which field failed +- **code**: Machine-readable error code +- **message**: Human-readable message +- **value**: The invalid value (be careful with sensitive data!) +- **Additional context**: Any constraints that were violated + +### Nested Field Validation + +For complex objects, use dot notation or JSON pointers: + +```javascript +{ + "errors": [ + { + "field": "shipping_address.zip_code", + "code": "invalid_format", + "message": "ZIP code must be 5 digits", + "value": "abcde" + }, + { + "field": "items[0].quantity", + "code": "out_of_range", + "message": "Quantity must be between 1 and 10", + "value": 0 + } + ] +} +``` + +## Error Codes: The Machine-Readable Layer + +HTTP status codes tell you the general category. Error codes tell you specifically what went wrong. + +### Designing Error Codes + +**Pattern 1: SCREAMING_SNAKE_CASE** +```javascript +{ + "error": { + "code": "INSUFFICIENT_CREDIT", + "message": "Your account has insufficient credit" + } +} +``` + +Pros: Easy to type, grep-able +Cons: Visually loud + +**Pattern 2: kebab-case** +```javascript +{ + "error": { + "code": "insufficient-credit", + "message": "Your account has insufficient credit" + } +} +``` + +Pros: Clean, URL-friendly +Cons: Harder to distinguish from field names + +**Pattern 3: PascalCase (Microsoft style)** +```javascript +{ + "error": { + "code": "InsufficientCredit", + "message": "Your account has insufficient credit" + } +} +``` + +Pros: Looks like class names, familiar to many developers +Cons: Can be confused with type names + +**Recommendation**: Pick one and be consistent. We prefer `SCREAMING_SNAKE_CASE` for error codes because they stand out. + +### Organizing Error Codes + +Group error codes by domain: + +``` +AUTH_* + AUTH_INVALID_CREDENTIALS + AUTH_TOKEN_EXPIRED + AUTH_TOKEN_INVALID + AUTH_INSUFFICIENT_PERMISSIONS + +VALIDATION_* + VALIDATION_REQUIRED_FIELD + VALIDATION_INVALID_FORMAT + VALIDATION_OUT_OF_RANGE + +RESOURCE_* + RESOURCE_NOT_FOUND + RESOURCE_ALREADY_EXISTS + RESOURCE_CONFLICT + +RATE_LIMIT_* + RATE_LIMIT_EXCEEDED + RATE_LIMIT_QUOTA_EXHAUSTED + +PAYMENT_* + PAYMENT_CARD_DECLINED + PAYMENT_INSUFFICIENT_FUNDS + PAYMENT_PROCESSING_ERROR +``` + +### Error Code Documentation + +For each error code, document: +- What it means +- When it occurs +- How to fix it +- Example request/response + +```markdown +## VALIDATION_INVALID_EMAIL + +**Status Code**: 422 Unprocessable Entity + +**Description**: The provided email address is not valid. + +**Common Causes**: +- Missing @ symbol +- No domain extension +- Contains invalid characters + +**How to Fix**: +- Validate email format before sending: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` +- Ensure email is URL-encoded if sent in query params + +**Example**: +```javascript +POST /api/users +{ + "email": "invalid.email" +} + +HTTP/1.1 422 Unprocessable Entity +{ + "error": { + "code": "VALIDATION_INVALID_EMAIL", + "message": "Invalid email format", + "field": "email" + } +} +``` +``` + +## The Request ID Pattern + +Every error response should include a request ID. This is your debugging lifeline. + +```javascript +{ + "error": { + "type": "https://api.example.com/errors/internal-error", + "title": "Internal Server Error", + "status": 500, + "detail": "An unexpected error occurred", + "request_id": "req_7x9k2m3n4p", // Critical for debugging! + "timestamp": "2024-01-08T14:30:00Z" + } +} +``` + +### Why Request IDs Matter + +**Scenario**: A user reports an error. + +**Without Request ID**: +``` +User: "I got an error when trying to update my profile." +Support: "When did this happen?" +User: "Um, maybe 2 PM yesterday?" +Support: *searches through millions of log entries* +``` + +**With Request ID**: +``` +User: "I got an error. The request ID is req_7x9k2m3n4p." +Support: *searches logs for exact request* +Support: "Found it. Your email was already taken." +``` + +### Implementing Request IDs + +```javascript +const { v4: uuidv4 } = require('uuid'); + +// Middleware to add request ID +app.use((req, res, next) => { + // Use existing request ID or generate new one + req.id = req.headers['x-request-id'] || `req_${uuidv4()}`; + + // Echo it back in response + res.setHeader('X-Request-ID', req.id); + + next(); +}); + +// Include in all error responses +app.use((err, req, res, next) => { + const error = { + type: getErrorType(err), + title: getErrorTitle(err), + status: getErrorStatus(err), + detail: err.message, + request_id: req.id, // Always include! + timestamp: new Date().toISOString() + }; + + // Log with request ID + logger.error({ + request_id: req.id, + error: err.message, + stack: err.stack + }); + + res.status(error.status).json(error); +}); +``` + +## Security: What Not to Expose + +Error messages can leak information to attackers. Here's what to avoid: + +### Don't Expose Internal Details + +**Bad**: +```javascript +{ + "error": "Uncaught MongoError: connect ECONNREFUSED 127.0.0.1:27017" +} +``` + +This tells an attacker: +- You're using MongoDB +- The database is on localhost +- The database is down + +**Good**: +```javascript +{ + "error": { + "type": "https://api.example.com/errors/service-unavailable", + "title": "Service Unavailable", + "status": 503, + "detail": "The service is temporarily unavailable", + "request_id": "req_abc123" + } +} +``` + +### Don't Expose Stack Traces in Production + +**Bad**: +```javascript +{ + "error": "Error: user not found", + "stack": "Error: user not found\n at UserController.getUser (src/controllers/user.js:42:15)\n at..." +} +``` + +**Good**: +```javascript +{ + "error": { + "type": "https://api.example.com/errors/not-found", + "title": "Not Found", + "status": 404, + "detail": "User not found", + "request_id": "req_abc123" + } +} +``` + +Log the stack trace server-side with the request ID. Support can look it up if needed. + +### Be Careful with User Enumeration + +**Bad**: +```javascript +POST /api/login +{"email": "alice@example.com", "password": "wrong"} + +// Different messages for existing vs non-existing users +{ + "error": "Incorrect password" // Tells attacker the email exists! +} +``` + +**Good**: +```javascript +POST /api/login +{"email": "alice@example.com", "password": "wrong"} + +{ + "error": { + "type": "https://api.example.com/errors/invalid-credentials", + "title": "Invalid Credentials", + "status": 401, + "detail": "Invalid email or password" // Doesn't reveal which + } +} +``` + +## Error Handling Across the Stack + +Errors can occur at multiple layers. Here's how to handle each: + +### Application Layer + +```javascript +// Domain-specific business logic errors +class InsufficientCreditError extends ApiError { + constructor(required, available) { + super({ + type: 'https://api.example.com/errors/insufficient-credit', + title: 'Insufficient Credit', + status: 403, + detail: `Required: $${required}, Available: $${available}`, + required, + available + }); + } +} + +// Throw in business logic +if (account.balance < amount) { + throw new InsufficientCreditError(amount, account.balance); +} +``` + +### Database Layer + +```javascript +try { + const user = await db.users.findOne({ email }); +} catch (err) { + if (err.code === 'ECONNREFUSED') { + // Database is down + throw new ApiError({ + type: 'https://api.example.com/errors/service-unavailable', + title: 'Service Unavailable', + status: 503, + detail: 'Service temporarily unavailable' + }); + } + + if (err.name === 'ValidationError') { + // Mongoose validation error + throw new ApiError({ + type: 'https://api.example.com/errors/validation-error', + title: 'Validation Error', + status: 422, + detail: 'Invalid data provided', + errors: formatMongooseErrors(err) + }); + } + + // Unknown error + throw err; +} +``` + +### External API Layer + +```javascript +try { + const response = await fetch('https://payment-provider.com/api/charge', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(chargeData) + }); + + if (!response.ok) { + throw new ApiError({ + type: 'https://api.example.com/errors/payment-failed', + title: 'Payment Failed', + status: response.status, + detail: 'Payment could not be processed', + provider_error: await response.text(), + request_id: req.id + }); + } +} catch (err) { + if (err.code === 'ETIMEDOUT') { + throw new ApiError({ + type: 'https://api.example.com/errors/gateway-timeout', + title: 'Gateway Timeout', + status: 504, + detail: 'Payment provider did not respond in time' + }); + } + + throw err; +} +``` + +## Global Error Handler + +Centralize error handling to ensure consistency: + +```javascript +// Express global error handler +app.use((err, req, res, next) => { + // Log all errors + logger.error({ + request_id: req.id, + error: err.message, + stack: err.stack, + url: req.url, + method: req.method, + user: req.user?.id + }); + + // Handle known error types + if (err instanceof ApiError) { + return res.status(err.status).json(err.toJSON()); + } + + // Handle validation errors + if (err.name === 'ValidationError') { + return res.status(422).json({ + type: 'https://api.example.com/errors/validation-error', + title: 'Validation Error', + status: 422, + detail: 'Invalid data provided', + errors: formatValidationErrors(err), + request_id: req.id + }); + } + + // Handle JWT errors + if (err.name === 'JsonWebTokenError') { + return res.status(401).json({ + type: 'https://api.example.com/errors/invalid-token', + title: 'Invalid Token', + status: 401, + detail: 'Authentication token is invalid', + request_id: req.id + }); + } + + // Unknown error - don't expose details + res.status(500).json({ + type: 'about:blank', + title: 'Internal Server Error', + status: 500, + detail: 'An unexpected error occurred', + request_id: req.id, + timestamp: new Date().toISOString() + }); +}); +``` + +## Testing Error Scenarios + +Don't just test the happy pathβ€”test your error handling: + +```javascript +describe('POST /api/users', () => { + it('returns 422 for invalid email', async () => { + const response = await request(app) + .post('/api/users') + .send({ email: 'invalid', name: 'Alice' }); + + expect(response.status).toBe(422); + expect(response.body).toMatchObject({ + type: expect.stringContaining('validation-error'), + title: 'Validation Error', + status: 422, + errors: expect.arrayContaining([ + expect.objectContaining({ + field: 'email', + code: 'invalid_format' + }) + ]) + }); + }); + + it('returns 409 for duplicate email', async () => { + // Create user + await createUser({ email: 'alice@example.com' }); + + // Try to create again + const response = await request(app) + .post('/api/users') + .send({ email: 'alice@example.com', name: 'Alice' }); + + expect(response.status).toBe(409); + expect(response.body.type).toContain('duplicate-resource'); + }); + + it('returns 503 when database is down', async () => { + // Mock database failure + jest.spyOn(db, 'users').mockRejectedValue( + new Error('ECONNREFUSED') + ); + + const response = await request(app) + .post('/api/users') + .send({ email: 'alice@example.com', name: 'Alice' }); + + expect(response.status).toBe(503); + expect(response.body.request_id).toBeDefined(); + }); +}); +``` + +## Client-Side Error Handling + +How should clients handle your errors? + +### Parse by Status Code Category + +```javascript +async function apiRequest(url, options) { + const response = await fetch(url, options); + + if (response.ok) { + return response.json(); + } + + const error = await response.json(); + + if (response.status >= 400 && response.status < 500) { + // Client error - don't retry + if (response.status === 401) { + // Redirect to login + redirectToLogin(); + } else if (response.status === 422) { + // Show validation errors to user + showValidationErrors(error.errors); + } else { + // Show generic error + showError(error.detail); + } + throw error; + } + + if (response.status >= 500) { + // Server error - might retry + if (response.status === 503 && error.retry_after) { + // Wait and retry + await sleep(error.retry_after * 1000); + return apiRequest(url, options); + } + + // Log for debugging + logError({ + request_id: error.request_id, + url, + status: response.status + }); + + showError('Service temporarily unavailable. Please try again.'); + throw error; + } +} +``` + +### Display User-Friendly Messages + +```javascript +function formatErrorForUser(error) { + // Use custom messages for common errors + const messages = { + 'VALIDATION_INVALID_EMAIL': 'Please enter a valid email address', + 'RATE_LIMIT_EXCEEDED': 'You\'re doing that too fast. Please slow down.', + 'INSUFFICIENT_CREDIT': 'You don\'t have enough credit for this operation' + }; + + return messages[error.code] || error.detail || 'Something went wrong'; +} +``` + +## Summary: Error Handling Checklist + +When implementing error handling: + +- [ ] Use appropriate HTTP status codes (4xx for client errors, 5xx for server errors) +- [ ] Follow RFC 7807 for error response format +- [ ] Include machine-readable error codes +- [ ] Generate and include request IDs +- [ ] Never expose internal details in production +- [ ] Document all error codes and their meanings +- [ ] Handle validation errors with field-level detail +- [ ] Log errors server-side with full context +- [ ] Test error scenarios, not just happy paths +- [ ] Provide helpful error messages that explain how to fix issues +- [ ] Use consistent error format across all endpoints +- [ ] Include `Retry-After` headers for rate limits and 503 errors + +Good error handling turns frustrating failures into guided recovery. Your users will thank you. + +## Further Reading + +- **RFC 7231**: HTTP Semantics (status codes) +- **RFC 7807**: Problem Details for HTTP APIs +- **RFC 8594**: Sunset HTTP Header +- **OWASP API Security**: Error handling best practices +- **JSON Schema**: For validating request bodies + +--- + +**Next**: Learn how to secure your API in [Authentication](./authentication.md). diff --git a/apis/versioning.md b/apis/versioning.md new file mode 100644 index 0000000..7e0e17c --- /dev/null +++ b/apis/versioning.md @@ -0,0 +1,580 @@ +# API Versioning: Planning for Change + +> **Your API will change. The question is: will you break your users when it does?** + +Every API starts simple. You launch with a clean design, a handful of endpoints, and everything works beautifully. Then reality hits: you need to add a new field, change a response format, orβ€”worseβ€”fix a fundamental design flaw. You have thousands of apps in production depending on your current API. What do you do? + +This is the versioning problem. Solve it well, and your API can evolve gracefully for years. Solve it poorly, and you'll either stagnate your API or break your users' applications. There's no middle ground. + +## The Problem: Evolution vs. Stability + +Here's the tension: your API needs to evolve to stay relevant, but your users need stability to avoid constant rewrites. A mobile app developer who integrated your API six months ago shouldn't wake up to find their app broken because you "improved" your API. + +### The Real Cost of Breaking Changes + +When you break an API, you don't just break codeβ€”you break trust: + +- Mobile apps that can't update fast enough crash for users +- Third-party integrations fail at 3 AM, waking up operations teams +- Partners lose confidence and start looking for alternatives +- Your support team drowns in tickets from angry developers + +Yet without changes, your API becomes a fossil, unable to support new features or fix design mistakes. + +## Understanding Versioning Strategies + +There are three main approaches to API versioning, each with different trade-offs. Let's explore them by examining how they handle a real scenario: you need to change how dates are formatted. + +### Strategy 1: URL Versioning + +**The Approach**: Put the version number in the URL path. + +``` +GET /api/v1/users +GET /api/v2/users +``` + +**Why It Works**: This is the most explicit and visible approach. Developers can see exactly which version they're using just by looking at the URL. Routing is straightforwardβ€”different URLs can point to completely different implementations. + +**Real Example**: +```javascript +// Version 1: dates as Unix timestamps +GET /api/v1/orders/123 +{ + "id": "123", + "created": 1704724800, // Unix timestamp + "status": "shipped" +} + +// Version 2: dates as ISO 8601 strings +GET /api/v2/orders/123 +{ + "id": "123", + "created": "2024-01-08T12:00:00Z", // ISO 8601 + "status": "shipped" +} +``` + +**When to Use**: URL versioning works best when: +- You're building a public API that many developers will consume +- You want version selection to be obvious and hard to mess up +- You're okay with maintaining multiple codebases or routing logic +- Your infrastructure makes routing by URL path simple (like with API gateways) + +**The Downside**: URLs change between versions, which means you're creating completely new resources from REST's perspective. You also need to maintain routing for each version. + +**Standards Reference**: While HTTP doesn't mandate how you structure URLs (RFC 3986 just defines URI syntax), this approach aligns with the principle that different resources should have different identifiers. + +### Strategy 2: Header Versioning + +**The Approach**: Keep URLs the same, but specify version in HTTP headers. + +``` +GET /api/users +Accept: application/vnd.myapi.v2+json +``` + +Or with a custom header: +``` +GET /api/users +API-Version: 2 +``` + +**Why It Works**: This follows HTTP's content negotiation pattern (RFC 7231). The same resource can have different representations based on what the client requests. It's more "RESTful" because the resource identifier (URL) doesn't changeβ€”only the representation does. + +**Real Example**: +```javascript +// Client requests version 1 +GET /api/orders/123 +Accept: application/vnd.myapi.v1+json + +Response: +{ + "id": "123", + "created": 1704724800, + "status": "shipped" +} + +// Client requests version 2 +GET /api/orders/123 +Accept: application/vnd.myapi.v2+json + +Response: +{ + "id": "123", + "created": "2024-01-08T12:00:00Z", + "status": "shipped" +} +``` + +**When to Use**: Header versioning shines when: +- You want clean, stable URLs +- Your API design treats versions as different representations of the same resource +- You're comfortable with content negotiation patterns +- You have sophisticated clients who can manage headers + +**The Downside**: Headers are invisible in browser address bars and harder to test with simple tools like cURL (though not impossible). Developers might forget to set them, leading to unexpected behavior. + +**Standards Reference**: This approach leverages HTTP's built-in content negotiation (RFC 7231, Section 5.3). The `Accept` header was specifically designed for this kind of versioning. + +### Strategy 3: Query Parameter Versioning + +**The Approach**: Add version as a query parameter. + +``` +GET /api/users?version=2 +``` + +**Why It Works**: It's simple to implement and understand. The version is visible in the URL (good) but doesn't change the resource path (also good?). + +**Real Example**: +```javascript +// Version 1 +GET /api/orders/123?version=1 +{ + "id": "123", + "created": 1704724800, + "status": "shipped" +} + +// Version 2 +GET /api/orders/123?version=2 +{ + "id": "123", + "created": "2024-01-08T12:00:00Z", + "status": "shipped" +} +``` + +**When to Use**: Query parameters work when: +- You want something simpler than header-based versioning +- You need versions to be visible in URLs +- You're okay with versions mixing with other query parameters + +**The Downside**: Query parameters typically modify resource selection or representation, not the API version. This can lead to confusion: is `?version=2` selecting a different resource or requesting a different format? Also, these URLs become harder to cache effectively. + +**Standards Reference**: RFC 3986 defines query strings for resource identification, but using them for versioning is more convention than standard. + +## Semantic Versioning for APIs + +Regardless of which strategy you choose, you need a system for deciding when to bump version numbers. This is where semantic versioning principles help. + +### The Version Number Format + +``` +MAJOR.MINOR.PATCH + 2 . 3 . 1 +``` + +**MAJOR**: Breaking changes that require client updates +- Removing an endpoint +- Removing or renaming a field +- Changing field types (string to number) +- Changing URL structures +- Changing authentication methods + +**MINOR**: New features that don't break existing clients +- Adding new endpoints +- Adding new optional fields to requests +- Adding new fields to responses (clients should ignore unknown fields) +- Adding new optional query parameters + +**PATCH**: Bug fixes and minor improvements +- Fixing incorrect behavior +- Performance improvements +- Documentation updates + +**Critical Rule**: Most API versioning uses only the MAJOR version in the URL or headers (`v1`, `v2`, `v3`). The MINOR and PATCH numbers are tracked internally but don't require clients to change. + +### Why? The Compatibility Contract + +Here's the key insight: if you design your API right, MINOR and PATCH changes are invisible to existing clients: + +```javascript +// Your API at v2.0.0 +GET /api/v2/users/123 +{ + "id": "123", + "name": "Alice", + "email": "alice@example.com" +} + +// You add a new field (v2.1.0 - MINOR change) +GET /api/v2/users/123 +{ + "id": "123", + "name": "Alice", + "email": "alice@example.com", + "phone": "+1-555-1234" // New field! +} + +// Well-designed clients ignore unknown fields +// They still work perfectly! +``` + +This works because of two principles: + +1. **Robustness Principle** (Postel's Law): "Be conservative in what you send, be liberal in what you accept" +2. **Forward Compatibility**: Clients should ignore fields they don't understand + +### Version Lifecycle Management + +Every version you release becomes a commitment. Here's a practical lifecycle: + +**Phase 1: Active (0-12 months)** +- Full support, all new features +- Bug fixes and security updates +- Recommended for new integrations + +**Phase 2: Deprecated (12-24 months)** +- Announced via headers: `Sunset: Sat, 31 Dec 2024 23:59:59 GMT` (RFC 8594) +- Security updates only +- Warning in documentation +- Migration guide published + +**Phase 3: Sunset (after 24 months)** +- Version removed or returns errors +- Sufficient warning given + +**Example Deprecation Header**: +``` +HTTP/1.1 200 OK +Sunset: Sat, 31 Dec 2024 23:59:59 GMT +Deprecation: true +Link: ; rel="sunset" + +{ + "data": "..." +} +``` + +## Implementation Patterns + +Let's look at how to actually build versioned APIs. + +### Pattern 1: Separate Codebases + +**The Approach**: Maintain completely separate code for each version. + +``` +src/ + v1/ + controllers/ + models/ + routes.js + v2/ + controllers/ + models/ + routes.js + shared/ + utils/ + database/ +``` + +**Pros**: +- Clean separation +- Can refactor v2 without touching v1 +- Easy to remove old versions + +**Cons**: +- Code duplication +- Bug fixes might need to be applied to multiple versions +- More code to maintain + +**When to Use**: When versions are significantly different or when you want complete isolation. + +### Pattern 2: Transformation Layer + +**The Approach**: Keep one internal model, transform for each version. + +```javascript +// Internal model +class Order { + constructor(data) { + this.id = data.id; + this.createdAt = data.created_at; // Always Date object internally + this.status = data.status; + } +} + +// Version 1 transformer +function toV1(order) { + return { + id: order.id, + created: Math.floor(order.createdAt.getTime() / 1000), // Unix timestamp + status: order.status + }; +} + +// Version 2 transformer +function toV2(order) { + return { + id: order.id, + created: order.createdAt.toISOString(), // ISO 8601 + status: order.status + }; +} + +// Route handler +app.get('/api/:version/orders/:id', async (req, res) => { + const order = await Order.findById(req.params.id); + + const transformer = req.params.version === 'v1' ? toV1 : toV2; + res.json(transformer(order)); +}); +``` + +**Pros**: +- Single source of truth for business logic +- Bug fixes automatically apply to all versions +- Less code duplication + +**Cons**: +- Transformers can get complex +- Hard to handle significantly different versions +- Testing needs to cover all transformations + +**When to Use**: When versions differ mainly in representation, not logic. + +### Pattern 3: Feature Flags + +**The Approach**: Use runtime flags to enable/disable features per version. + +```javascript +const features = { + v1: { + isoDateFormat: false, + includeMetadata: false, + expandedErrors: false + }, + v2: { + isoDateFormat: true, + includeMetadata: true, + expandedErrors: true + } +}; + +function formatOrder(order, version) { + const config = features[version]; + + return { + id: order.id, + created: config.isoDateFormat + ? order.createdAt.toISOString() + : Math.floor(order.createdAt.getTime() / 1000), + status: order.status, + ...(config.includeMetadata && { + metadata: order.metadata + }) + }; +} +``` + +**Pros**: +- Flexible +- Can gradually roll out features +- Easy to A/B test + +**Cons**: +- Can lead to spaghetti code +- Hard to reason about all combinations +- Removing old versions is tricky + +**When to Use**: For gradual rollouts or when versions have subtle differences. + +## Making Version Changes Safe + +When you need to introduce a breaking change, how do you do it safely? + +### Step 1: Expand Phase + +Add the new field/behavior alongside the old: + +```javascript +// v2.0 - old way (created is Unix timestamp) +{ + "id": "123", + "created": 1704724800, + "status": "shipped" +} + +// v2.1 - MINOR: add new field, keep old (EXPAND) +{ + "id": "123", + "created": 1704724800, // Keep old field + "created_at": "2024-01-08T12:00:00Z", // Add new field + "status": "shipped" +} +``` + +### Step 2: Migrate Phase + +Give clients time to migrate (6-12 months): +- Update documentation +- Send emails to registered developers +- Add deprecation headers +- Monitor usage of old field + +### Step 3: Contract Phase + +In next MAJOR version, remove the old field: + +```javascript +// v3.0 - MAJOR: remove old field (CONTRACT) +{ + "id": "123", + "created_at": "2024-01-08T12:00:00Z", // Only new field + "status": "shipped" +} +``` + +This is the "Expand-Contract" pattern. Never go straight from old to new in a breaking way. + +## Real-World Example: Stripe's Versioning + +Stripe has one of the most mature API versioning strategies. Let's see what we can learn: + +**Version Format**: Date-based (e.g., `2024-01-08`) + +``` +GET /v1/charges +Stripe-Version: 2024-01-08 +``` + +**Why Dates?**: Every change gets a dated version. Developers can upgrade at their own pace. + +**Compatibility**: Stripe maintains old versions for *years*. They've never broken backward compatibility since launch. + +**Account-Level Pinning**: Each API key is pinned to a version. You can upgrade when ready: + +```javascript +// Your account created in 2023 +// Your API calls use 2023-01-01 version by default + +// When you're ready, override for specific calls +fetch('https://api.stripe.com/v1/charges', { + headers: { + 'Stripe-Version': '2024-01-08' // Opt into new version + } +}); +``` + +**What This Teaches Us**: +- Pin versions to accounts/API keys, not just requests +- Use dates for fine-grained version control +- Never force upgrades +- Provide upgrade tools and testing environments + +## Common Pitfalls and How to Avoid Them + +### Pitfall 1: Too Many Versions + +**The Problem**: Maintaining v1, v2, v3, v4, v5 simultaneously becomes unsustainable. + +**The Solution**: +- Set clear deprecation timelines +- Only support 2-3 major versions at once +- Make minor/patch changes backward compatible + +### Pitfall 2: Version Leakage + +**The Problem**: Internal systems reference specific versions, creating tight coupling. + +```javascript +// Bad: internal code knows about versions +class OrderService { + formatForV1(order) { ... } + formatForV2(order) { ... } + formatForV3(order) { ... } +} +``` + +**The Solution**: Keep version logic at the API boundary: + +```javascript +// Good: internal code version-agnostic +class OrderService { + getOrder(id) { + return this.database.findOne(id); // Returns domain model + } +} + +// Transformation happens in API layer +app.get('/api/:version/orders/:id', async (req, res) => { + const order = await orderService.getOrder(req.params.id); + const transformer = getTransformer(req.params.version); + res.json(transformer(order)); +}); +``` + +### Pitfall 3: Incomplete Version Coverage + +**The Problem**: You version endpoints but not error responses, headers, or webhooks. + +**The Solution**: Version everything: +- API responses +- Error formats +- Webhook payloads +- WebSocket messages +- Rate limit headers + +### Pitfall 4: No Migration Path + +**The Problem**: You announce "v1 is deprecated" but give no guidance on migration. + +**The Solution**: Provide: +- Detailed migration guides +- Code examples for common changes +- Automated migration tools where possible +- Testing environment with new version + +## Decision Framework + +Still not sure which versioning strategy to use? Use this decision tree: + +**Question 1: Is this a public API for external developers?** +- Yes β†’ Use URL versioning (`/api/v1/users`) +- No (internal only) β†’ Header versioning might be fine + +**Question 2: How often will you make breaking changes?** +- Rarely (once a year or less) β†’ URL versioning +- Frequently (monthly) β†’ Consider if you're making too many breaking changes! + +**Question 3: How sophisticated are your clients?** +- Mix of skill levels β†’ URL versioning (most visible) +- All experienced developers β†’ Header versioning (more "REST-ful") + +**Question 4: How long will you support old versions?** +- Years β†’ URL versioning (easier to maintain separate) +- Months β†’ Transformation layer might work + +**For most teams**: Start with URL versioning in the format `/api/v{MAJOR}/resource`. It's explicit, easy to understand, and hard to mess up. + +## Summary: The Versioning Checklist + +When planning your versioning strategy: + +- [ ] Choose a versioning scheme (URL, header, or query) +- [ ] Define what constitutes MAJOR vs. MINOR changes +- [ ] Set deprecation timeline (recommended: 12-24 months) +- [ ] Implement version detection in your routing +- [ ] Add deprecation headers to old versions +- [ ] Create migration guides for each major version +- [ ] Monitor usage of different versions +- [ ] Plan removal strategy for old versions +- [ ] Version not just responses but errors, webhooks, everything +- [ ] Test version transitions thoroughly + +The goal isn't to never change your APIβ€”it's to change it without breaking trust. + +## Further Reading + +- **RFC 7231**: HTTP Semantics (covers content negotiation) +- **RFC 8594**: The Sunset HTTP Header (deprecation signaling) +- **Semantic Versioning**: https://semver.org +- **REST Dissertation**: Roy Fielding's original REST definition +- **API Evolution Patterns**: Martin Fowler's refactoring catalog + +--- + +**Next**: Learn how to communicate failures gracefully in [Error Handling](./error-handling.md). diff --git a/frontend.md b/frontend.md deleted file mode 100644 index cf0033c..0000000 --- a/frontend.md +++ /dev/null @@ -1,1629 +0,0 @@ -# Frontend Best Practices - -A comprehensive guide to building modern, performant, and accessible frontend applications based on web standards and industry best practices. - -## Table of Contents - -1. [Project Structure](#project-structure) -2. [Performance](#performance) -3. [Accessibility](#accessibility) -4. [State Management](#state-management) -5. [Routing](#routing) -6. [Forms & Validation](#forms--validation) -7. [API Integration](#api-integration) -8. [Error Handling](#error-handling) -9. [Testing](#testing) -10. [Security](#security) -11. [Build & Deployment](#build--deployment) -12. [Developer Experience](#developer-experience) - ---- - -## Project Structure - -Organize your codebase for maintainability and scalability. - -### Recommended Structure - -``` -src/ -β”œβ”€β”€ assets/ # Static files (images, fonts, icons) -β”œβ”€β”€ components/ # Reusable UI components -β”‚ β”œβ”€β”€ common/ # Shared components (Button, Input, etc.) -β”‚ β”œβ”€β”€ layout/ # Layout components (Header, Footer, Sidebar) -β”‚ └── features/ # Feature-specific components -β”œβ”€β”€ hooks/ # Custom React hooks / composables -β”œβ”€β”€ services/ # API services and external integrations -β”œβ”€β”€ utils/ # Helper functions and utilities -β”œβ”€β”€ types/ # TypeScript types and interfaces -β”œβ”€β”€ styles/ # Global styles and theme -β”œβ”€β”€ pages/ # Page components / route views -β”œβ”€β”€ store/ # State management (Redux, Zustand, etc.) -β”œβ”€β”€ config/ # Configuration files -β”œβ”€β”€ constants/ # Application constants -└── tests/ # Test utilities and setup -``` - -### Component Organization - -``` -components/ -└── Button/ - β”œβ”€β”€ Button.tsx # Component implementation - β”œβ”€β”€ Button.test.tsx # Tests - β”œβ”€β”€ Button.stories.tsx # Storybook stories - β”œβ”€β”€ Button.module.css # Component styles - └── index.ts # Public exports -``` - -### Best Practices - -- Use feature-based folders for large applications -- Keep components small and focused (Single Responsibility) -- Separate business logic from UI components -- Use barrel exports (index.ts) for cleaner imports -- Colocate related files (component, styles, tests) -- Avoid deep nesting (max 3-4 levels) - ---- - -## Performance - -Optimize for speed and user experience. - -### Core Web Vitals - -Monitor and optimize these key metrics: - -**Largest Contentful Paint (LCP)** - < 2.5s -- Optimize images (WebP, AVIF formats) -- Use lazy loading -- Minimize render-blocking resources -- Use CDN for static assets - -**First Input Delay (FID)** - < 100ms -- Minimize JavaScript execution -- Code splitting -- Use web workers for heavy computations -- Optimize event handlers - -**Cumulative Layout Shift (CLS)** - < 0.1 -- Set explicit dimensions for images/videos -- Avoid inserting content above existing content -- Use CSS transforms instead of layout properties - -### Code Splitting - -```javascript -// Route-based splitting -const Home = lazy(() => import('./pages/Home')); -const About = lazy(() => import('./pages/About')); -const Dashboard = lazy(() => import('./pages/Dashboard')); - -// Component-based splitting -const HeavyChart = lazy(() => import('./components/HeavyChart')); -``` - -### Image Optimization - -```jsx -// Responsive images -Hero image - -// Modern formats with fallback - - - - Hero image - -``` - -### Lazy Loading - -```javascript -// Intersection Observer for lazy loading -const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - img.src = img.dataset.src; - observer.unobserve(img); - } - }); -}); - -document.querySelectorAll('img[data-src]').forEach(img => { - observer.observe(img); -}); -``` - -### Memoization - -```javascript -// React.memo for component memoization -const ExpensiveComponent = React.memo(({ data }) => { - return
{/* expensive render */}
; -}); - -// useMemo for expensive calculations -const expensiveValue = useMemo(() => { - return computeExpensiveValue(a, b); -}, [a, b]); - -// useCallback for function memoization -const handleClick = useCallback(() => { - doSomething(a, b); -}, [a, b]); -``` - -### Bundle Optimization - -```javascript -// Webpack configuration -module.exports = { - optimization: { - splitChunks: { - chunks: 'all', - cacheGroups: { - vendor: { - test: /[\\/]node_modules[\\/]/, - name: 'vendors', - priority: 10 - } - } - } - } -}; -``` - -### Best Practices - -- Compress assets (gzip, brotli) -- Use HTTP/2 or HTTP/3 -- Implement service workers for caching -- Minimize third-party scripts -- Use resource hints (preload, prefetch, preconnect) -- Optimize fonts (subset, preload) -- Remove unused CSS/JS -- Use tree shaking -- Monitor bundle size - ---- - -## Accessibility - -Build inclusive applications for all users. - -### Semantic HTML - -```html - - - -
-
-

Article Title

-

Content...

-
-
- - - - - -``` - -### ARIA Attributes - -```jsx -// Button with accessible name - - -// Live region for dynamic content -
- {statusMessage} -
- -// Disclosure widget - - - -// Form with proper labels - - -{hasError && {errorMessage}} -``` - -### Keyboard Navigation - -```javascript -// Ensure interactive elements are keyboard accessible -const handleKeyDown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleClick(); - } -}; - -
- Click me -
-``` - -### Focus Management - -```javascript -// Trap focus in modal -useEffect(() => { - if (isOpen) { - const focusableElements = modal.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - ); - const firstElement = focusableElements[0]; - const lastElement = focusableElements[focusableElements.length - 1]; - - firstElement?.focus(); - - const handleTab = (e) => { - if (e.key === 'Tab') { - if (e.shiftKey && document.activeElement === firstElement) { - e.preventDefault(); - lastElement?.focus(); - } else if (!e.shiftKey && document.activeElement === lastElement) { - e.preventDefault(); - firstElement?.focus(); - } - } - }; - - modal.addEventListener('keydown', handleTab); - return () => modal.removeEventListener('keydown', handleTab); - } -}, [isOpen]); -``` - -### Color Contrast - -```css -/* Ensure minimum contrast ratios */ -/* WCAG AA: 4.5:1 for normal text, 3:1 for large text */ -/* WCAG AAA: 7:1 for normal text, 4.5:1 for large text */ - -.button { - background-color: #0066cc; - color: #ffffff; /* 7.3:1 contrast ratio */ -} - -.text { - color: #333333; /* Against white: 12.6:1 */ -} -``` - -### Screen Reader Support - -```jsx -// Skip to main content - - Skip to main content - - -
- {/* content */} -
- -// Announce route changes -useEffect(() => { - const announcement = document.createElement('div'); - announcement.setAttribute('role', 'status'); - announcement.setAttribute('aria-live', 'polite'); - announcement.textContent = `Navigated to ${pageTitle}`; - document.body.appendChild(announcement); - - setTimeout(() => announcement.remove(), 1000); -}, [location]); -``` - -### Best Practices - -- Use semantic HTML elements -- Provide text alternatives for images -- Ensure keyboard navigation works -- Maintain sufficient color contrast -- Support screen readers with ARIA -- Test with accessibility tools (axe, Lighthouse) -- Don't rely on color alone to convey information -- Provide focus indicators -- Make clickable areas large enough (min 44x44px) -- Test with actual screen readers - ---- - -## State Management - -Manage application state effectively. - -### Local State - -```javascript -// useState for component state -const [count, setCount] = useState(0); -const [user, setUser] = useState(null); - -// useReducer for complex state logic -const [state, dispatch] = useReducer(reducer, initialState); - -function reducer(state, action) { - switch (action.type) { - case 'increment': - return { count: state.count + 1 }; - case 'decrement': - return { count: state.count - 1 }; - default: - return state; - } -} -``` - -### Context API - -```javascript -// Theme context -const ThemeContext = createContext(); - -export function ThemeProvider({ children }) { - const [theme, setTheme] = useState('light'); - - const value = { - theme, - toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') - }; - - return ( - - {children} - - ); -} - -export function useTheme() { - const context = useContext(ThemeContext); - if (!context) { - throw new Error('useTheme must be used within ThemeProvider'); - } - return context; -} -``` - -### Global State (Redux/Zustand) - -```javascript -// Zustand store -import create from 'zustand'; - -const useStore = create((set) => ({ - user: null, - isAuthenticated: false, - login: (userData) => set({ user: userData, isAuthenticated: true }), - logout: () => set({ user: null, isAuthenticated: false }) -})); - -// Usage -function Profile() { - const user = useStore(state => state.user); - const logout = useStore(state => state.logout); - - return
{user?.name}
; -} -``` - -### Server State (React Query) - -```javascript -// Fetching data with React Query -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; - -function Users() { - const { data, isLoading, error } = useQuery({ - queryKey: ['users'], - queryFn: fetchUsers - }); - - const queryClient = useQueryClient(); - - const createUser = useMutation({ - mutationFn: createUserAPI, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['users'] }); - } - }); - - if (isLoading) return
Loading...
; - if (error) return
Error: {error.message}
; - - return
{/* render users */}
; -} -``` - -### Best Practices - -- Keep state as local as possible -- Lift state only when necessary -- Use Context for theme, auth, i18n -- Use specialized libraries for server state -- Avoid prop drilling (use composition or context) -- Normalize nested/relational data -- Use selectors to derive state -- Keep state immutable - ---- - -## Routing - -Implement client-side routing effectively. - -### React Router Example - -```javascript -import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; - -function App() { - return ( - - - - - } /> - } /> - } /> - } /> - } /> - - - ); -} -``` - -### Nested Routes - -```javascript -}> - } /> - } /> - } /> - - -// Dashboard component -function Dashboard() { - return ( -
- - {/* Renders nested routes */} -
- ); -} -``` - -### Protected Routes - -```javascript -function ProtectedRoute({ children }) { - const { isAuthenticated } = useAuth(); - - if (!isAuthenticated) { - return ; - } - - return children; -} - -// Usage - - - - } -/> -``` - -### Route Parameters & Search Params - -```javascript -// URL: /users/123?tab=profile -function UserDetail() { - const { id } = useParams(); - const [searchParams] = useSearchParams(); - const tab = searchParams.get('tab'); - - return
User {id}, Tab: {tab}
; -} -``` - -### Programmatic Navigation - -```javascript -function LoginForm() { - const navigate = useNavigate(); - - const handleSubmit = async (data) => { - await login(data); - navigate('/dashboard', { replace: true }); - }; - - return
{/* form */}
; -} -``` - -### Best Practices - -- Use code splitting for routes -- Implement loading states -- Handle 404 pages -- Use meaningful URLs -- Implement breadcrumbs for deep navigation -- Preserve scroll position when appropriate -- Use `replace` for redirects after actions -- Implement route-based analytics - ---- - -## Forms & Validation - -Build robust forms with proper validation. - -### Controlled Components - -```javascript -function LoginForm() { - const [formData, setFormData] = useState({ - email: '', - password: '' - }); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; - - const handleSubmit = (e) => { - e.preventDefault(); - // Handle submission - }; - - return ( -
- - - -
- ); -} -``` - -### Form Libraries (React Hook Form) - -```javascript -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import * as z from 'zod'; - -const schema = z.object({ - email: z.string().email('Invalid email address'), - password: z.string().min(8, 'Password must be at least 8 characters'), - age: z.number().min(18, 'Must be at least 18 years old') -}); - -function RegisterForm() { - const { - register, - handleSubmit, - formState: { errors, isSubmitting } - } = useForm({ - resolver: zodResolver(schema) - }); - - const onSubmit = async (data) => { - await createUser(data); - }; - - return ( -
-
- - {errors.email && {errors.email.message}} -
- -
- - {errors.password && {errors.password.message}} -
- - -
- ); -} -``` - -### Custom Validation - -```javascript -const validateEmail = (value) => { - if (!value) return 'Email is required'; - if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'; - return true; -}; - -const validatePassword = (value) => { - if (!value) return 'Password is required'; - if (value.length < 8) return 'Password must be at least 8 characters'; - if (!/[A-Z]/.test(value)) return 'Must contain uppercase letter'; - if (!/[a-z]/.test(value)) return 'Must contain lowercase letter'; - if (!/[0-9]/.test(value)) return 'Must contain number'; - return true; -}; -``` - -### File Upload - -```javascript -function FileUpload() { - const [file, setFile] = useState(null); - const [preview, setPreview] = useState(null); - - const handleFileChange = (e) => { - const selectedFile = e.target.files[0]; - - if (selectedFile) { - // Validate file - if (selectedFile.size > 5 * 1024 * 1024) { - alert('File too large (max 5MB)'); - return; - } - - if (!['image/jpeg', 'image/png'].includes(selectedFile.type)) { - alert('Invalid file type'); - return; - } - - setFile(selectedFile); - setPreview(URL.createObjectURL(selectedFile)); - } - }; - - const handleUpload = async () => { - const formData = new FormData(); - formData.append('file', file); - - await fetch('/api/upload', { - method: 'POST', - body: formData - }); - }; - - return ( -
- - {preview && Preview} - -
- ); -} -``` - -### Best Practices - -- Provide immediate feedback on validation -- Show clear error messages -- Disable submit during processing -- Preserve form data on errors -- Use proper input types (email, tel, number) -- Implement client-side and server-side validation -- Provide helpful placeholder text -- Use autocomplete attributes -- Handle loading and error states -- Consider accessibility in form design - ---- - -## API Integration - -Connect frontend to backend services. - -### Fetch API - -```javascript -async function fetchUsers() { - try { - const response = await fetch('/api/users', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error('Failed to fetch users:', error); - throw error; - } -} -``` - -### API Service Layer - -```javascript -// services/api.js -const API_BASE_URL = process.env.REACT_APP_API_URL; - -class ApiService { - constructor() { - this.baseURL = API_BASE_URL; - } - - async request(endpoint, options = {}) { - const url = `${this.baseURL}${endpoint}`; - const token = localStorage.getItem('token'); - - const config = { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(token && { 'Authorization': `Bearer ${token}` }), - ...options.headers - } - }; - - try { - const response = await fetch(url, config); - - if (!response.ok) { - const error = await response.json(); - throw new ApiError(error.message, response.status, error); - } - - return await response.json(); - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError('Network error', 0, error); - } - } - - get(endpoint, options) { - return this.request(endpoint, { ...options, method: 'GET' }); - } - - post(endpoint, data, options) { - return this.request(endpoint, { - ...options, - method: 'POST', - body: JSON.stringify(data) - }); - } - - put(endpoint, data, options) { - return this.request(endpoint, { - ...options, - method: 'PUT', - body: JSON.stringify(data) - }); - } - - delete(endpoint, options) { - return this.request(endpoint, { ...options, method: 'DELETE' }); - } -} - -class ApiError extends Error { - constructor(message, status, details) { - super(message); - this.status = status; - this.details = details; - } -} - -export const api = new ApiService(); - -// services/users.js -export const userService = { - getUsers: () => api.get('/users'), - getUser: (id) => api.get(`/users/${id}`), - createUser: (data) => api.post('/users', data), - updateUser: (id, data) => api.put(`/users/${id}`, data), - deleteUser: (id) => api.delete(`/users/${id}`) -}; -``` - -### Request Interceptors - -```javascript -// Add request interceptor for auth token -const originalFetch = window.fetch; -window.fetch = async (...args) => { - const [url, config = {}] = args; - - // Add auth token - const token = localStorage.getItem('token'); - if (token) { - config.headers = { - ...config.headers, - 'Authorization': `Bearer ${token}` - }; - } - - // Add request ID - config.headers = { - ...config.headers, - 'X-Request-ID': generateRequestId() - }; - - const response = await originalFetch(url, config); - - // Handle 401 (unauthorized) - if (response.status === 401) { - // Redirect to login - window.location.href = '/login'; - } - - return response; -}; -``` - -### Optimistic Updates - -```javascript -function TodoList() { - const [todos, setTodos] = useState([]); - - const toggleTodo = async (id) => { - // Optimistic update - setTodos(prev => - prev.map(todo => - todo.id === id ? { ...todo, completed: !todo.completed } : todo - ) - ); - - try { - await api.put(`/todos/${id}/toggle`); - } catch (error) { - // Revert on error - setTodos(prev => - prev.map(todo => - todo.id === id ? { ...todo, completed: !todo.completed } : todo - ) - ); - showError('Failed to update todo'); - } - }; - - return
{/* render todos */}
; -} -``` - -### Best Practices - -- Centralize API calls in service layer -- Handle errors consistently -- Implement request/response interceptors -- Add loading states -- Implement retry logic for failed requests -- Use environment variables for API URLs -- Add request timeouts -- Cache responses when appropriate -- Implement optimistic updates for better UX -- Handle offline scenarios - ---- - -## Error Handling - -Gracefully handle errors for better user experience. - -### Error Boundaries (React) - -```javascript -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error) { - return { hasError: true, error }; - } - - componentDidCatch(error, errorInfo) { - // Log to error reporting service - console.error('Error caught:', error, errorInfo); - logErrorToService(error, errorInfo); - } - - render() { - if (this.state.hasError) { - return ( -
-

Something went wrong

-

{this.state.error?.message}

- -
- ); - } - - return this.props.children; - } -} - -// Usage - - - -``` - -### Try-Catch Pattern - -```javascript -async function loadUserData() { - try { - setLoading(true); - setError(null); - - const user = await fetchUser(); - const posts = await fetchUserPosts(user.id); - - setUser(user); - setPosts(posts); - } catch (error) { - if (error.status === 404) { - setError('User not found'); - } else if (error.status >= 500) { - setError('Server error. Please try again later.'); - } else { - setError('An unexpected error occurred'); - } - - // Log error - logError(error); - } finally { - setLoading(false); - } -} -``` - -### Toast Notifications - -```javascript -import { toast } from 'react-hot-toast'; - -// Success -toast.success('Settings saved successfully!'); - -// Error -toast.error('Failed to save settings'); - -// Custom -toast.custom((t) => ( -
-

Custom notification

- -
-)); -``` - -### Global Error Handler - -```javascript -// Catch unhandled promise rejections -window.addEventListener('unhandledrejection', (event) => { - console.error('Unhandled promise rejection:', event.reason); - logErrorToService(event.reason); - - toast.error('An unexpected error occurred'); - event.preventDefault(); -}); - -// Catch global errors -window.addEventListener('error', (event) => { - console.error('Global error:', event.error); - logErrorToService(event.error); -}); -``` - -### Best Practices - -- Use error boundaries for component errors -- Provide user-friendly error messages -- Log errors to monitoring service -- Show appropriate fallback UI -- Handle network errors gracefully -- Implement retry mechanisms -- Don't expose sensitive error details to users -- Provide recovery options -- Handle different error types appropriately - ---- - -## Testing - -Ensure code quality with comprehensive testing. - -### Unit Tests (Jest + React Testing Library) - -```javascript -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { Counter } from './Counter'; - -describe('Counter', () => { - it('renders initial count', () => { - render(); - expect(screen.getByText('Count: 0')).toBeInTheDocument(); - }); - - it('increments count when button clicked', async () => { - const user = userEvent.setup(); - render(); - - const button = screen.getByRole('button', { name: /increment/i }); - await user.click(button); - - expect(screen.getByText('Count: 1')).toBeInTheDocument(); - }); - - it('calls onCountChange with new value', async () => { - const user = userEvent.setup(); - const handleCountChange = jest.fn(); - render(); - - const button = screen.getByRole('button', { name: /increment/i }); - await user.click(button); - - expect(handleCountChange).toHaveBeenCalledWith(1); - }); -}); -``` - -### Integration Tests - -```javascript -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { UserProfile } from './UserProfile'; -import { server } from './mocks/server'; -import { rest } from 'msw'; - -describe('UserProfile Integration', () => { - it('loads and displays user data', async () => { - render(); - - expect(screen.getByText(/loading/i)).toBeInTheDocument(); - - await waitFor(() => { - expect(screen.getByText('John Doe')).toBeInTheDocument(); - }); - }); - - it('handles server error', async () => { - server.use( - rest.get('/api/users/:id', (req, res, ctx) => { - return res(ctx.status(500)); - }) - ); - - render(); - - await waitFor(() => { - expect(screen.getByText(/error/i)).toBeInTheDocument(); - }); - }); -}); -``` - -### E2E Tests (Playwright/Cypress) - -```javascript -// Playwright -import { test, expect } from '@playwright/test'; - -test('user can login and view dashboard', async ({ page }) => { - await page.goto('/login'); - - await page.fill('[name="email"]', 'user@example.com'); - await page.fill('[name="password"]', 'password123'); - await page.click('button[type="submit"]'); - - await expect(page).toHaveURL('/dashboard'); - await expect(page.locator('h1')).toContainText('Dashboard'); -}); - -// Cypress -describe('Login Flow', () => { - it('allows user to login', () => { - cy.visit('/login'); - cy.get('[name="email"]').type('user@example.com'); - cy.get('[name="password"]').type('password123'); - cy.get('button[type="submit"]').click(); - - cy.url().should('include', '/dashboard'); - cy.contains('h1', 'Dashboard').should('be.visible'); - }); -}); -``` - -### Test Coverage - -```json -{ - "jest": { - "collectCoverageFrom": [ - "src/**/*.{js,jsx,ts,tsx}", - "!src/**/*.test.{js,jsx,ts,tsx}", - "!src/index.tsx" - ], - "coverageThreshold": { - "global": { - "branches": 80, - "functions": 80, - "lines": 80, - "statements": 80 - } - } - } -} -``` - -### Best Practices - -- Write tests for critical user paths -- Test behavior, not implementation -- Use data-testid sparingly (prefer accessible queries) -- Mock external dependencies -- Test error states and edge cases -- Maintain test coverage above 80% -- Run tests in CI/CD pipeline -- Use visual regression testing for UI -- Keep tests fast and isolated - ---- - -## Security - -Protect your application and users. - -### XSS Prevention - -```javascript -// Good: React escapes by default -
{userInput}
- -// Dangerous: Avoid dangerouslySetInnerHTML -
// ❌ - -// If needed, sanitize first -import DOMPurify from 'dompurify'; -
-``` - -### CSRF Protection - -```javascript -// Include CSRF token in requests -const csrfToken = document.querySelector('meta[name="csrf-token"]').content; - -fetch('/api/data', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': csrfToken - }, - body: JSON.stringify(data) -}); -``` - -### Secure Storage - -```javascript -// Never store sensitive data in localStorage -// ❌ Don't do this -localStorage.setItem('creditCard', cardNumber); - -// βœ… Store in httpOnly cookies (set by server) -// βœ… Or use sessionStorage for temporary data -sessionStorage.setItem('tempData', data); - -// Clear on logout -function logout() { - localStorage.clear(); - sessionStorage.clear(); - // Redirect to login -} -``` - -### Content Security Policy - -```html - -``` - -### Input Validation - -```javascript -function sanitizeInput(input) { - // Remove HTML tags - return input.replace(/<[^>]*>/g, ''); -} - -function validateEmail(email) { - const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return regex.test(email); -} - -function validateURL(url) { - try { - new URL(url); - return true; - } catch { - return false; - } -} -``` - -### Best Practices - -- Sanitize all user input -- Use HTTPS everywhere -- Implement CSP headers -- Avoid inline scripts and styles -- Use httpOnly and secure flags for cookies -- Implement proper CORS policies -- Keep dependencies updated -- Use security headers (X-Frame-Options, etc.) -- Implement rate limiting on client side -- Never expose API keys in client code - ---- - -## Build & Deployment - -Optimize for production deployment. - -### Environment Variables - -```bash -# .env.development -REACT_APP_API_URL=http://localhost:3000/api -REACT_APP_ENV=development - -# .env.production -REACT_APP_API_URL=https://api.production.com -REACT_APP_ENV=production -``` - -```javascript -const apiUrl = process.env.REACT_APP_API_URL; -const isDevelopment = process.env.NODE_ENV === 'development'; -``` - -### Build Optimization - -```javascript -// vite.config.js -export default { - build: { - minify: 'terser', - sourcemap: false, - rollupOptions: { - output: { - manualChunks: { - vendor: ['react', 'react-dom'], - router: ['react-router-dom'] - } - } - } - } -}; -``` - -### Progressive Web App - -```javascript -// service-worker.js -self.addEventListener('install', (event) => { - event.waitUntil( - caches.open('v1').then((cache) => { - return cache.addAll([ - '/', - '/index.html', - '/styles.css', - '/app.js' - ]); - }) - ); -}); - -self.addEventListener('fetch', (event) => { - event.respondWith( - caches.match(event.request).then((response) => { - return response || fetch(event.request); - }) - ); -}); -``` - -### CI/CD Pipeline - -```yaml -# .github/workflows/deploy.yml -name: Deploy - -on: - push: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: '18' - - - run: npm ci - - run: npm run lint - - run: npm test - - run: npm run build - - - name: Deploy to Production - run: npm run deploy - env: - DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} -``` - -### Best Practices - -- Use environment variables for configuration -- Minify and compress assets -- Remove console.logs in production -- Enable source maps for debugging (stored separately) -- Implement caching strategies -- Use CDN for static assets -- Monitor bundle size -- Set up automated deployments -- Implement rollback mechanisms -- Use feature flags for gradual rollouts - ---- - -## Developer Experience - -Improve productivity and code quality. - -### Code Formatting (Prettier) - -```json -{ - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "printWidth": 80 -} -``` - -### Linting (ESLint) - -```json -{ - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-script/recommended" - ], - "rules": { - "no-console": "warn", - "no-unused-vars": "error", - "react/prop-types": "off" - } -} -``` - -### Git Hooks (Husky) - -```json -{ - "husky": { - "hooks": { - "pre-commit": "lint-staged", - "pre-push": "npm test" - } - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint --fix", - "prettier --write" - ] - } -} -``` - -### TypeScript - -```typescript -// Properly typed components -interface ButtonProps { - variant?: 'primary' | 'secondary'; - size?: 'small' | 'medium' | 'large'; - onClick?: () => void; - children: React.ReactNode; -} - -const Button: React.FC = ({ - variant = 'primary', - size = 'medium', - onClick, - children -}) => { - return ( - - ); -}; -``` - -### Component Documentation (Storybook) - -```javascript -// Button.stories.tsx -export default { - title: 'Components/Button', - component: Button, - argTypes: { - variant: { - control: 'select', - options: ['primary', 'secondary'] - } - } -}; - -export const Primary = { - args: { - variant: 'primary', - children: 'Click me' - } -}; - -export const Secondary = { - args: { - variant: 'secondary', - children: 'Click me' - } -}; -``` - -### Best Practices - -- Use TypeScript for type safety -- Set up automated formatting and linting -- Document components with Storybook -- Use git hooks to enforce quality -- Implement code review process -- Use consistent naming conventions -- Write meaningful commit messages -- Keep dependencies updated -- Use absolute imports for better organization -- Implement hot module replacement for faster development - ---- - -## Summary - -Following these frontend best practices will help you build modern, performant, and maintainable applications: - -1. **Organize code** with clear structure and separation of concerns -2. **Optimize performance** with code splitting, lazy loading, and caching -3. **Build accessible** applications that work for everyone -4. **Manage state** effectively with appropriate tools -5. **Implement routing** for seamless navigation -6. **Validate forms** with user-friendly error handling -7. **Integrate APIs** through a clean service layer -8. **Handle errors** gracefully with proper fallbacks -9. **Test thoroughly** with unit, integration, and E2E tests -10. **Secure your app** against common vulnerabilities -11. **Deploy efficiently** with optimized builds and CI/CD -12. **Enhance DX** with tooling and automation - -These standards create a foundation for frontend applications that are fast, reliable, accessible, and maintainable. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..5247029 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,235 @@ +# Frontend Development Best Practices + +> **Building user experiences that work for everyone, everywhere** + +The frontend is where your users live. It's the buttons they click, the forms they fill out, the pages they navigate. Get it right, and your application feels fast, accessible, and intuitive. Get it wrong, and users abandon your product before they even understand what it does. + +This section covers everything you need to build modern, production-grade frontend applications based on web standards and real-world patterns. + +## What You'll Learn + +Each guide in this section builds your understanding from foundational concepts to advanced implementation: + +### Core Topics + +1. **Project Structure** - Organizing code for maintainability and scalability +2. **Performance** - Making your application fast using Web Performance APIs +3. **Accessibility** - Building for all users following WCAG guidelines +4. **State Management** - Managing application state effectively +5. **Routing** - Client-side navigation and deep linking +6. **Forms & Validation** - Collecting and validating user input +7. **API Integration** - Connecting to backend services +8. **Error Handling** - Graceful degradation and recovery +9. **Testing** - Ensuring quality with automated tests +10. **Security** - Protecting users from XSS, CSRF, and other attacks +11. **Build & Deployment** - Optimizing for production +12. **Developer Experience** - Tools and workflows for productivity + +## The Modern Frontend Landscape + +Frontend development has evolved dramatically. What started as simple HTML and JavaScript has grown into a sophisticated ecosystem of frameworks, build tools, and architectural patterns. + +### The Core Technologies + +No matter which framework you choose, these web platform APIs form the foundation: + +**DOM (Document Object Model)** - W3C standard for representing HTML +**Web APIs** - fetch(), localStorage, IntersectionObserver, etc. +**CSS** - Styling and layout +**ECMAScript** - The JavaScript language specification + +Everything else (React, Vue, Angular, Svelte) is built on top of these standards. + +### Framework vs. Library vs. Vanilla + +**Frameworks** (Angular, Ember) +- Opinionated, full-featured +- Everything included +- Steeper learning curve + +**Libraries** (React, Vue, Svelte) +- Focused on UI +- Compose with other tools +- More flexibility + +**Vanilla JavaScript** +- No dependencies +- Full control +- More code to write + +**Recommendation**: For most projects, start with a popular library (React, Vue, or Svelte) and build up from there. You'll get community support, established patterns, and a rich ecosystem. + +## Web Standards First + +Before diving into framework-specific patterns, understand the web platform itself: + +### The HTML Living Standard + +HTML isn't just markupβ€”it's a sophisticated system for semantic structure, accessibility, and progressive enhancement. + +**Semantic Elements**: `
`, `