Claude Cursor Skill

api-design-patterns

Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices

LLM Mart · 0 points · 0 views 5 listing impressions 0 install-command copies

#architecture

Virus-scanned Reviewed automatically before listing.

Full trust report

Download aaaaqwq-agi-super-team-skills_api-design-patterns-cdb04e8.zip · 44 KB
Part of aaaaqwq/agi-super-team — 46 skills

Install

skills CLI npx skills add https://github.com/aAAaqwq/AGI-Super-Team/tree/main/skills/api-design-patterns
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aaaaqwq-agi-super-team@llmmart
Git git clone https://github.com/aAAaqwq/AGI-Super-Team.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aaaaqwq/agi-super-team collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

API Design Patterns

Design robust, scalable APIs using proven patterns for REST, GraphQL, and gRPC with proper versioning, authentication, and error handling.

Quick Reference

API Style Selection:

  • REST: Resource-based CRUD, simple clients, HTTP-native caching
  • GraphQL: Client-driven queries, complex data graphs, real-time subscriptions
  • gRPC: High-performance RPC, microservices, strong typing, streaming

Critical Patterns:

  • Versioning: URI (/v1/users), header (Accept: application/vnd.api+json;version=1), content negotiation
  • Pagination: Offset (simple), cursor (stable), keyset (performant)
  • Auth: OAuth2 (delegated), JWT (stateless), API keys (service-to-service)
  • Rate limiting: Token bucket, fixed window, sliding window
  • Idempotency: Idempotency keys, conditional requests, safe retry

See references/ for deep dives: rest-patterns.md, graphql-patterns.md, grpc-patterns.md, versioning-strategies.md, authentication.md

Core Principles

Universal API Design Standards

Apply these principles across all API styles:

1. Consistency Over Cleverness

  • Follow established conventions for your API style
  • Use predictable naming patterns (snake_case or camelCase, pick one)
  • Maintain consistent error response formats
  • Version breaking changes, never surprise clients

2. Design for Evolution

  • Plan for versioning from day one
  • Use optional fields with sensible defaults
  • Deprecate gracefully with sunset dates
  • Document breaking vs non-breaking changes

3. Security by Default

  • Require authentication unless explicitly public
  • Use HTTPS/TLS for all production endpoints
  • Implement rate limiting and throttling
  • Validate and sanitize all inputs
  • Return minimal error details to clients

4. Developer Experience First

  • Provide comprehensive documentation (OpenAPI, GraphQL schema)
  • Return meaningful error messages with actionable guidance
  • Use standard HTTP status codes correctly
  • Include request IDs for debugging
  • Offer SDKs and code generators

API Style Decision Tree

When to Choose REST

✅ Use REST when:

  • Building CRUD-focused resource APIs
  • Clients need HTTP caching (ETags, Cache-Control)
  • Wide platform compatibility required (browsers, mobile, IoT)
  • Simple, stateless client-server model fits
  • Team familiar with HTTP/REST conventions

❌ Avoid REST when:

  • Complex data fetching with nested relationships (N+1 queries)
  • Real-time updates are primary use case
  • Need strong typing and code generation
  • High-performance RPC between microservices

Example Use Cases: Public APIs, mobile backends, traditional web services

When to Choose GraphQL

✅ Use GraphQL when:

  • Clients need flexible, client-driven queries
  • Complex data graphs with nested relationships
  • Multiple client types with different data needs
  • Real-time subscriptions required
  • Strong typing and schema validation needed

❌ Avoid GraphQL when:

  • Simple CRUD operations dominate
  • HTTP caching is critical (GraphQL uses POST)
  • File uploads are primary feature (requires extensions)
  • Team lacks GraphQL expertise
  • Performance optimization is complex (N+1 problem)

Example Use Cases: Client-facing APIs, dashboards, mobile apps with varied UIs

When to Choose gRPC

✅ Use gRPC when:

  • Microservice-to-microservice communication
  • High performance and low latency critical
  • Bidirectional streaming needed
  • Strong typing with Protocol Buffers
  • Polyglot environments (language interop)

❌ Avoid gRPC when:

  • Browser clients (limited support, needs grpc-web)
  • HTTP/JSON required for compatibility
  • Human-readable payloads preferred
  • Simple request/response patterns

Example Use Cases: Internal microservices, streaming data, service mesh

REST API Patterns

Resource Naming

✅ Good: Plural nouns, hierarchical

GET    /users              # List users
GET    /users/123          # Get user
POST   /users              # Create user
PUT    /users/123          # Update user (full)
PATCH  /users/123          # Update user (partial)
DELETE /users/123          # Delete user
GET    /users/123/orders   # User's orders (sub-resource)

❌ Bad: Verbs, mixed conventions

GET    /getUsers           # Don't use verbs
POST   /user/create        # Don't use verbs
GET    /Users/123          # Don't capitalize
GET    /user/123           # Don't mix singular/plural

HTTP Status Codes

Success Codes:

  • 200 OK: Successful GET, PUT, PATCH, DELETE with body
  • 201 Created: Successful POST, return Location header
  • 202 Accepted: Async operation started
  • 204 No Content: Successful DELETE, no body

Client Error Codes:

  • 400 Bad Request: Invalid input, validation error
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: Authenticated but insufficient permissions
  • 404 Not Found: Resource doesn't exist
  • 409 Conflict: State conflict (duplicate, version mismatch)
  • 422 Unprocessable Entity: Semantic validation error
  • 429 Too Many Requests: Rate limit exceeded

Server Error Codes:

  • 500 Internal Server Error: Unexpected error
  • 502 Bad Gateway: Upstream service error
  • 503 Service Unavailable: Temporary outage
  • 504 Gateway Timeout: Upstream timeout

Error Response Format

✅ Consistent error structure

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format",
        "code": "INVALID_FORMAT"
      }
    ],
    "request_id": "req_abc123",
    "documentation_url": "https://api.example.com/docs/errors/validation"
  }
}

Pagination Patterns

Offset Pagination (simple, familiar):

GET /users?limit=20&offset=40

✅ Use for: Small datasets, admin interfaces ❌ Avoid for: Large datasets (skips become expensive), real-time data

Cursor Pagination (stable, efficient):

GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }

✅ Use for: Infinite scroll, real-time feeds, large datasets ❌ Avoid for: Random access, page numbers

Keyset Pagination (performant):

GET /users?limit=20&after_id=123

✅ Use for: Ordered data, database index friendly ❌ Avoid for: Complex sorting, multiple sort keys

See references/rest-patterns.md for filtering, sorting, field selection, HATEOAS

GraphQL Patterns

Schema Design

✅ Good: Clear types, nullable by default

type User {
  id: ID!                    # Non-null ID
  email: String!             # Required field
  name: String               # Optional (nullable by default)
  createdAt: DateTime!
  orders: [Order!]!          # Non-null array of non-null orders
}

type Query {
  user(id: ID!): User
  users(first: Int, after: String): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
}

input CreateUserInput {
  email: String!
  name: String
}

type CreateUserPayload {
  user: User
  userEdge: UserEdge
  errors: [UserError!]
}

Resolver Patterns

Avoid N+1 Queries with DataLoader:

import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds: string[]) => {
  const users = await db.users.findMany({ where: { id: { in: userIds } } });
  return userIds.map(id => users.find(u => u.id === id));
});

// Resolver batches queries automatically
const resolvers = {
  Order: {
    user: (order) => userLoader.load(order.userId)
  }
};

Query Complexity Analysis

Prevent expensive queries:

import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  schema,
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log('Query cost:', cost),
    }),
  ],
});

See references/graphql-patterns.md for subscriptions, relay cursor connections, error handling

gRPC Patterns

Service Definition

syntax = "proto3";

package users.v1;

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {}
  rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
  rpc CreateUser (CreateUserRequest) returns (User) {}
  rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
  rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
}

message User {
  string id = 1;
  string email = 2;
  string name = 3;
  google.protobuf.Timestamp created_at = 4;
}

message GetUserRequest {
  string id = 1;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  string next_page_token = 2;
}

Error Handling

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    if req.Id == "" {
        return nil, status.Error(codes.InvalidArgument, "user ID is required")
    }

    user, err := s.db.GetUser(ctx, req.Id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, status.Error(codes.NotFound, "user not found")
        }
        return nil, status.Error(codes.Internal, "database error")
    }

    return user, nil
}

See references/grpc-patterns.md for streaming, interceptors, metadata, health checks

Versioning Strategies

URI Versioning (Simple, Explicit)

✅ Most common, easy to understand

GET /v1/users/123
GET /v2/users/123

Pros: Clear, easy to route, browser-friendly Cons: Couples version to URL, duplicates routes

Header Versioning (Clean URLs)

GET /users/123
Accept: application/vnd.myapi.v2+json

Pros: Clean URLs, version separate from resource Cons: Less visible, harder to test manually

Content Negotiation (Granular)

GET /users/123
Accept: application/vnd.myapi.user.v2+json

Pros: Resource-level versioning, backward compatible Cons: Complex, harder to implement

Version Deprecation Process

{
  "version": "1.0",
  "deprecated": true,
  "sunset_date": "2025-12-31",
  "migration_guide": "https://docs.api.com/v1-to-v2",
  "replacement_version": "2.0"
}

Include deprecation warnings:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"

See references/versioning-strategies.md for detailed migration patterns

Authentication & Authorization

OAuth 2.0 (Delegated Access)

Use for: Third-party access, user consent, token refresh

Authorization Code Flow (most secure for web/mobile):

1. Client redirects to /authorize
2. User authenticates, grants permissions
3. Auth server redirects to callback with code
4. Client exchanges code for access token
5. Client uses access token for API requests
# Request token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET

# Response
{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
  "scope": "read write"
}

# Use token
GET /v1/users/me
Authorization: Bearer eyJhbGc...

JWT (Stateless Auth)

Use for: Microservices, stateless API auth, short-lived tokens

✅ Good: Minimal claims, short expiry

{
  "sub": "user_123",
  "iat": 1516239022,
  "exp": 1516242622,
  "scope": "read:users write:orders"
}

Validation:

import jwt from 'jsonwebtoken';

const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = payload.sub;

API Keys (Service-to-Service)

Use for: Server-to-server, CLI tools, webhooks

GET /v1/users
X-API-Key: sk_live_abc123...

# Or query parameter (less secure)
GET /v1/users?api_key=sk_live_abc123

Key Practices:

  • Prefix keys with environment (sk_live_, sk_test_)
  • Hash keys before storage (bcrypt, scrypt)
  • Allow key rotation without downtime
  • Support multiple keys per user
  • Rate limit per key

See references/authentication.md for API key rotation, scopes, RBAC

Rate Limiting

Token Bucket (Burst-Friendly)

Bucket: 100 tokens, refill 10/second
Request costs 1 token
Allows bursts up to bucket size

Headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200

429 Response:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 60 seconds.",
    "limit": 100,
    "reset_at": "2025-01-01T00:00:00Z"
  }
}

Sliding Window (Fair Distribution)

Counts requests in rolling time window. More accurate than fixed window.

Per-User vs Per-IP

  • Per-User: Authenticated requests, fair quotas
  • Per-IP: Unauthenticated requests, prevent abuse
  • Combined: Both limits, take stricter

Idempotency

Idempotent Methods (HTTP Spec)

Naturally Idempotent: GET, PUT, DELETE, HEAD, OPTIONS Not Idempotent: POST, PATCH

Idempotency Keys

Make POST requests idempotent:

POST /v1/payments
Idempotency-Key: uuid-or-client-generated-key
Content-Type: application/json

{
  "amount": 1000,
  "currency": "USD",
  "customer": "cust_123"
}

Server behavior:

  1. First request: Process and store result with key
  2. Duplicate request (same key): Return stored result (200 or 201)
  3. Different request (same key): Return 409 Conflict

Implementation:

const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
  const cached = await redis.get(`idempotency:${idempotencyKey}`);
  if (cached) {
    return res.status(cached.status).json(cached.body);
  }
}

const result = await processPayment(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
  status: 201,
  body: result
});

Conditional Requests

Use ETags for safe updates:

# Get resource with ETag
GET /v1/users/123
Response: ETag: "abc123"

# Update only if unchanged
PUT /v1/users/123
If-Match: "abc123"

# 412 Precondition Failed if ETag changed

Caching Strategies

HTTP Caching Headers

# Public, cacheable for 1 hour
Cache-Control: public, max-age=3600

# Private (user-specific), revalidate
Cache-Control: private, must-revalidate, max-age=0

# No caching
Cache-Control: no-store, no-cache, must-revalidate

ETag Validation

# Server returns ETag
GET /v1/users/123
Response:
  ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  Cache-Control: max-age=3600

# Client conditional request
GET /v1/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 304 Not Modified if unchanged (saves bandwidth)
HTTP/1.1 304 Not Modified

Last-Modified

GET /v1/users/123
Response:
  Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

# Conditional request
GET /v1/users/123
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

# 304 Not Modified if not modified

Webhooks

Event Delivery

POST https://client.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1640995200

{
  "id": "evt_abc123",
  "type": "payment.succeeded",
  "created": 1640995200,
  "data": {
    "object": {
      "id": "pay_123",
      "amount": 1000,
      "status": "succeeded"
    }
  }
}

Signature Verification

import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

Retry Strategy

  • Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s
  • Timeout: 5-30 seconds per attempt
  • Max attempts: 3-7 attempts
  • Dead letter queue: Store failed events
  • Manual retry: UI for re-sending failed events

API Documentation

OpenAPI/Swagger (REST)

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
components:
  schemas:
    User:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string

GraphQL Schema (Self-Documenting)

GraphQL introspection provides automatic documentation. Use descriptions:

"""
Represents a user account in the system.
Created via the createUser mutation.
"""
type User {
  """Unique identifier for the user"""
  id: ID!

  """Email address, must be unique"""
  email: String!

  """Optional display name"""
  name: String
}

API Documentation Best Practices

  1. Interactive examples: Provide working code samples
  2. Authentication guide: Step-by-step auth setup
  3. Error catalog: Document all error codes with examples
  4. Rate limits: Clearly state limits and headers
  5. Changelog: Track breaking and non-breaking changes
  6. Migration guides: Version upgrade instructions
  7. SDKs: Provide client libraries for popular languages

Anti-Patterns

❌ Over-fetching (REST): Returning entire objects when fields are unused ✅ Solution: Support field selection (?fields=id,name,email)

❌ Under-fetching (REST): Requiring multiple requests for related data ✅ Solution: Support expansion (?expand=orders,profile) or use GraphQL

❌ Chatty APIs: Too many round-trips for common operations ✅ Solution: Batch endpoints, compound documents, or GraphQL

❌ Ignoring HTTP semantics: Using GET for mutations, wrong status codes ✅ Solution: Follow HTTP spec, use correct methods and status codes

❌ Exposing internal structure: URLs/schemas mirror database ✅ Solution: Design resource-oriented APIs independent of storage

❌ Missing versioning: Breaking changes without version increments ✅ Solution: Version from day one, never break existing versions

❌ Poor error messages: Generic "An error occurred" ✅ Solution: Specific, actionable error messages with codes

❌ No rate limiting: APIs vulnerable to abuse ✅ Solution: Implement rate limiting from the start

Testing Strategies

Contract Testing

// Pact contract test
import { PactV3 } from '@pact-foundation/pact';

const provider = new PactV3({
  consumer: 'FrontendApp',
  provider: 'UserAPI'
});

it('gets a user by ID', () => {
  provider
    .given('user 123 exists')
    .uponReceiving('a request for user 123')
    .withRequest({
      method: 'GET',
      path: '/users/123'
    })
    .willRespondWith({
      status: 200,
      body: { id: '123', email: 'user@example.com' }
    });
});

Load Testing

// k6 load test
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m', target: 20 },
    { duration: '10s', target: 0 }
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% under 500ms
    http_req_failed: ['rate<0.01']    // <1% errors
  }
};

export default function () {
  const res = http.get('https://api.example.com/users');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500
  });
}

Related Skills

  • graphql: Deep GraphQL schema design, resolvers, Apollo Server
  • typescript: Type-safe API clients and servers
  • nodejs-backend: Express/Fastify REST API implementation
  • django: Django REST Framework patterns
  • fastapi: FastAPI Python REST/GraphQL APIs
  • flask: Flask-RESTful patterns

References

  • rest-patterns.md: Deep REST coverage (HATEOAS, filtering, field selection)
  • graphql-patterns.md: GraphQL subscriptions, relay cursor connections, federation
  • grpc-patterns.md: Streaming patterns, interceptors, service mesh integration
  • versioning-strategies.md: Detailed versioning approaches and migration patterns
  • authentication.md: OAuth flows, JWT best practices, API key rotation, RBAC

Additional Resources

Files (agi-super-team)
  • references
    • authentication.md 23.3 KB
      # API Authentication & Authorization - Deep Dive
      
      Comprehensive authentication and authorization patterns for APIs including OAuth 2.0, JWT, API keys, RBAC, and security best practices.
      
      ## Authentication vs Authorization
      
      **Authentication**: Who are you? (Identity verification)
      **Authorization**: What can you do? (Permission checking)
      
      ```
      Authentication → Who is making the request?
      Authorization → Is this user allowed to perform this action?
      ```
      
      ## OAuth 2.0
      
      ### Grant Types
      
      #### Authorization Code Flow (Most Secure)
      
      **Use for**: Web applications, mobile apps with backend
      
      ```
      1. User clicks "Login" on client
      2. Client redirects to /authorize
      3. User authenticates and grants permissions
      4. Auth server redirects to callback with authorization code
      5. Client exchanges code for access token (server-to-server)
      6. Client uses access token for API requests
      ```
      
      **Step-by-step**:
      
      ```http
      # 1. Client redirects to authorization endpoint
      https://auth.example.com/oauth/authorize?
        response_type=code&
        client_id=CLIENT_ID&
        redirect_uri=https://client.com/callback&
        scope=read:users write:orders&
        state=random_csrf_token
      
      # 2. User authenticates and approves
      
      # 3. Auth server redirects to callback
      https://client.com/callback?
        code=AUTH_CODE&
        state=random_csrf_token
      
      # 4. Client exchanges code for token (server-side)
      POST https://auth.example.com/oauth/token
      Content-Type: application/x-www-form-urlencoded
      
      grant_type=authorization_code&
      code=AUTH_CODE&
      redirect_uri=https://client.com/callback&
      client_id=CLIENT_ID&
      client_secret=CLIENT_SECRET
      
      # 5. Response with access token
      {
        "access_token": "eyJhbGc...",
        "token_type": "Bearer",
        "expires_in": 3600,
        "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
        "scope": "read:users write:orders"
      }
      
      # 6. Use access token
      GET https://api.example.com/users
      Authorization: Bearer eyJhbGc...
      ```
      
      #### Authorization Code Flow with PKCE
      
      **Use for**: Mobile apps, SPAs (no client secret)
      
      **PKCE (Proof Key for Code Exchange)** prevents authorization code interception.
      
      ```
      1. Client generates code_verifier (random string)
      2. Client creates code_challenge = SHA256(code_verifier)
      3. Client includes code_challenge in /authorize request
      4. Auth server stores code_challenge with authorization code
      5. Client includes code_verifier in token exchange
      6. Auth server verifies SHA256(code_verifier) == code_challenge
      ```
      
      **Implementation**:
      
      ```typescript
      import crypto from 'crypto';
      
      // Generate code verifier (43-128 characters)
      const codeVerifier = crypto.randomBytes(32).toString('base64url');
      
      // Generate code challenge
      const codeChallenge = crypto
        .createHash('sha256')
        .update(codeVerifier)
        .digest('base64url');
      
      // Step 1: Redirect to authorization
      const authUrl = new URL('https://auth.example.com/oauth/authorize');
      authUrl.searchParams.set('response_type', 'code');
      authUrl.searchParams.set('client_id', 'CLIENT_ID');
      authUrl.searchParams.set('redirect_uri', 'https://client.com/callback');
      authUrl.searchParams.set('code_challenge', codeChallenge);
      authUrl.searchParams.set('code_challenge_method', 'S256');
      authUrl.searchParams.set('scope', 'read:users');
      authUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex'));
      
      // Step 2: Exchange code for token
      const response = await fetch('https://auth.example.com/oauth/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          code: authCode,
          redirect_uri: 'https://client.com/callback',
          client_id: 'CLIENT_ID',
          code_verifier: codeVerifier, // Send original verifier
        }),
      });
      ```
      
      #### Client Credentials Flow
      
      **Use for**: Server-to-server, microservices, background jobs
      
      ```http
      POST https://auth.example.com/oauth/token
      Content-Type: application/x-www-form-urlencoded
      
      grant_type=client_credentials&
      client_id=CLIENT_ID&
      client_secret=CLIENT_SECRET&
      scope=read:users
      
      # Response
      {
        "access_token": "eyJhbGc...",
        "token_type": "Bearer",
        "expires_in": 3600,
        "scope": "read:users"
      }
      ```
      
      **No user context** - acts as the application itself.
      
      #### Implicit Flow (Deprecated)
      
      **Do not use**: Replaced by Authorization Code + PKCE
      
      ```http
      # Returns token directly in URL fragment (insecure)
      https://client.com/callback#access_token=TOKEN&...
      ```
      
      ❌ **Security issues**:
      - Token exposed in URL (browser history, referrer)
      - No refresh token
      - No client authentication
      
      #### Resource Owner Password Credentials (Avoid)
      
      **Use only for**: Trusted first-party apps (migration scenarios)
      
      ```http
      POST https://auth.example.com/oauth/token
      
      grant_type=password&
      username=user@example.com&
      password=secretpassword&
      client_id=CLIENT_ID&
      client_secret=CLIENT_SECRET
      ```
      
      ❌ **Avoid because**:
      - Client handles user password (security risk)
      - Doesn't support MFA
      - No consent screen
      - Use Authorization Code instead
      
      ### Token Types
      
      #### Access Token
      
      **Purpose**: Short-lived token for API access
      
      ```json
      {
        "access_token": "eyJhbGc...",
        "token_type": "Bearer",
        "expires_in": 3600
      }
      ```
      
      **Characteristics**:
      - Short expiration (15 min - 1 hour)
      - Contains permissions (scopes)
      - Can be opaque or JWT
      - Sent in Authorization header
      
      #### Refresh Token
      
      **Purpose**: Long-lived token to get new access tokens
      
      ```http
      POST https://auth.example.com/oauth/token
      
      grant_type=refresh_token&
      refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&
      client_id=CLIENT_ID&
      client_secret=CLIENT_SECRET
      
      # Response: New access token
      {
        "access_token": "new_access_token",
        "token_type": "Bearer",
        "expires_in": 3600,
        "refresh_token": "new_refresh_token"
      }
      ```
      
      **Characteristics**:
      - Long expiration (days to months)
      - Stored securely (encrypted database)
      - Can be revoked
      - May rotate on use (refresh token rotation)
      
      ### Scopes
      
      **Define permission boundaries**:
      
      ```
      read:users         - Read user data
      write:users        - Create/update users
      delete:users       - Delete users
      admin:users        - Full user management
      
      read:orders        - Read orders
      write:orders       - Create/update orders
      ```
      
      **Request specific scopes**:
      ```http
      GET /oauth/authorize?scope=read:users write:orders
      ```
      
      **Check scopes in API**:
      ```typescript
      function requireScope(requiredScope: string) {
        return (req, res, next) => {
          const tokenScopes = req.token.scope.split(' ');
      
          if (!tokenScopes.includes(requiredScope)) {
            return res.status(403).json({
              error: 'insufficient_scope',
              message: `Requires scope: ${requiredScope}`,
            });
          }
      
          next();
        };
      }
      
      app.get('/users', requireScope('read:users'), async (req, res) => {
        // Handler
      });
      ```
      
      ### Implementation (Node.js)
      
      **Auth server using oauth2-server**:
      ```typescript
      import OAuth2Server from 'oauth2-server';
      
      const oauth = new OAuth2Server({
        model: {
          // Get client by ID
          getClient: async (clientId, clientSecret) => {
            const client = await db.clients.findUnique({ where: { clientId } });
      
            if (!client || client.clientSecret !== clientSecret) {
              return null;
            }
      
            return {
              id: client.id,
              redirectUris: client.redirectUris,
              grants: client.grants,
            };
          },
      
          // Save authorization code
          saveAuthorizationCode: async (code, client, user) => {
            return db.authorizationCodes.create({
              data: {
                code: code.authorizationCode,
                expiresAt: code.expiresAt,
                redirectUri: code.redirectUri,
                clientId: client.id,
                userId: user.id,
              },
            });
          },
      
          // Get authorization code
          getAuthorizationCode: async (code) => {
            const authCode = await db.authorizationCodes.findUnique({
              where: { code },
              include: { client: true, user: true },
            });
      
            return {
              code: authCode.code,
              expiresAt: authCode.expiresAt,
              redirectUri: authCode.redirectUri,
              client: authCode.client,
              user: authCode.user,
            };
          },
      
          // Revoke authorization code
          revokeAuthorizationCode: async (code) => {
            await db.authorizationCodes.delete({ where: { code: code.code } });
            return true;
          },
      
          // Save access token
          saveToken: async (token, client, user) => {
            return db.accessTokens.create({
              data: {
                accessToken: token.accessToken,
                accessTokenExpiresAt: token.accessTokenExpiresAt,
                refreshToken: token.refreshToken,
                refreshTokenExpiresAt: token.refreshTokenExpiresAt,
                clientId: client.id,
                userId: user.id,
              },
            });
          },
      
          // Get access token
          getAccessToken: async (accessToken) => {
            const token = await db.accessTokens.findUnique({
              where: { accessToken },
              include: { client: true, user: true },
            });
      
            return {
              accessToken: token.accessToken,
              accessTokenExpiresAt: token.accessTokenExpiresAt,
              client: token.client,
              user: token.user,
            };
          },
      
          // Get refresh token
          getRefreshToken: async (refreshToken) => {
            const token = await db.accessTokens.findUnique({
              where: { refreshToken },
              include: { client: true, user: true },
            });
      
            return {
              refreshToken: token.refreshToken,
              refreshTokenExpiresAt: token.refreshTokenExpiresAt,
              client: token.client,
              user: token.user,
            };
          },
      
          // Revoke refresh token
          revokeToken: async (token) => {
            await db.accessTokens.delete({ where: { refreshToken: token.refreshToken } });
            return true;
          },
        },
      });
      
      // Endpoints
      app.post('/oauth/authorize', async (req, res) => {
        const request = new OAuth2Server.Request(req);
        const response = new OAuth2Server.Response(res);
      
        try {
          const code = await oauth.authorize(request, response);
          res.redirect(`${code.redirectUri}?code=${code.authorizationCode}&state=${req.query.state}`);
        } catch (err) {
          res.status(err.code || 500).json({ error: err.name, message: err.message });
        }
      });
      
      app.post('/oauth/token', async (req, res) => {
        const request = new OAuth2Server.Request(req);
        const response = new OAuth2Server.Response(res);
      
        try {
          const token = await oauth.token(request, response);
          res.json(token);
        } catch (err) {
          res.status(err.code || 500).json({ error: err.name, message: err.message });
        }
      });
      ```
      
      ## JWT (JSON Web Tokens)
      
      ### Structure
      
      ```
      HEADER.PAYLOAD.SIGNATURE
      ```
      
      **Header**:
      ```json
      {
        "alg": "HS256",
        "typ": "JWT"
      }
      ```
      
      **Payload** (claims):
      ```json
      {
        "sub": "user_123",
        "iat": 1516239022,
        "exp": 1516242622,
        "iss": "https://auth.example.com",
        "aud": "https://api.example.com",
        "scope": "read:users write:orders"
      }
      ```
      
      **Signature**:
      ```
      HMACSHA256(
        base64UrlEncode(header) + "." + base64UrlEncode(payload),
        secret
      )
      ```
      
      ### Standard Claims
      
      - `iss` (Issuer): Who issued the token
      - `sub` (Subject): User ID
      - `aud` (Audience): Who the token is for
      - `exp` (Expiration): Unix timestamp
      - `nbf` (Not Before): Unix timestamp
      - `iat` (Issued At): Unix timestamp
      - `jti` (JWT ID): Unique token ID
      
      ### Custom Claims
      
      ```json
      {
        "sub": "user_123",
        "email": "user@example.com",
        "role": "admin",
        "permissions": ["read:users", "write:users"],
        "org_id": "org_456"
      }
      ```
      
      ### Creating JWTs
      
      ```typescript
      import jwt from 'jsonwebtoken';
      
      const payload = {
        sub: 'user_123',
        email: 'user@example.com',
        role: 'admin',
      };
      
      const token = jwt.sign(payload, process.env.JWT_SECRET, {
        expiresIn: '1h',
        issuer: 'https://auth.example.com',
        audience: 'https://api.example.com',
      });
      ```
      
      ### Verifying JWTs
      
      ```typescript
      import jwt from 'jsonwebtoken';
      
      function verifyToken(token: string) {
        try {
          const payload = jwt.verify(token, process.env.JWT_SECRET, {
            issuer: 'https://auth.example.com',
            audience: 'https://api.example.com',
          });
      
          return payload;
        } catch (error) {
          if (error.name === 'TokenExpiredError') {
            throw new Error('Token expired');
          } else if (error.name === 'JsonWebTokenError') {
            throw new Error('Invalid token');
          }
          throw error;
        }
      }
      
      // Middleware
      app.use((req, res, next) => {
        const authHeader = req.headers.authorization;
      
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
          return res.status(401).json({ error: 'Missing or invalid authorization header' });
        }
      
        const token = authHeader.split(' ')[1];
      
        try {
          req.user = verifyToken(token);
          next();
        } catch (error) {
          return res.status(401).json({ error: error.message });
        }
      });
      ```
      
      ### Asymmetric Keys (RS256)
      
      **More secure**: Private key signs, public key verifies
      
      ```typescript
      import fs from 'fs';
      
      const privateKey = fs.readFileSync('private.key');
      const publicKey = fs.readFileSync('public.key');
      
      // Sign with private key
      const token = jwt.sign(payload, privateKey, {
        algorithm: 'RS256',
        expiresIn: '1h',
      });
      
      // Verify with public key
      const payload = jwt.verify(token, publicKey, {
        algorithms: ['RS256'],
      });
      ```
      
      **Benefits**:
      - API servers only need public key (can't create tokens)
      - Key rotation easier (distribute public keys)
      - More secure (private key never leaves auth server)
      
      ### JWT Best Practices
      
      ✅ **Use short expiration**: 15 minutes to 1 hour
      ✅ **Use asymmetric keys (RS256)**: More secure than symmetric (HS256)
      ✅ **Include minimal claims**: Tokens sent with every request
      ✅ **Validate all claims**: `iss`, `aud`, `exp`, `nbf`
      ✅ **Use JTI for revocation**: Track token IDs in database
      ✅ **Store refresh tokens**: Don't extend JWT expiration
      
      ❌ **Don't store sensitive data**: JWTs are not encrypted (only signed)
      ❌ **Don't use long expiration**: Hard to revoke
      ❌ **Don't skip validation**: Always verify signature and claims
      ❌ **Don't trust client-provided JWTs**: Always verify signature
      
      ## API Keys
      
      ### Types
      
      **Service-to-Service**:
      ```
      sk_live_abc123...
      sk_test_xyz789...
      ```
      
      **User API Keys**:
      ```
      pk_live_user123_abc...
      pk_test_user123_xyz...
      ```
      
      ### Key Format
      
      **Prefix** (identifies environment and type):
      - `sk_live_`: Live secret key
      - `sk_test_`: Test secret key
      - `pk_live_`: Live publishable key
      - `pk_test_`: Test publishable key
      
      **Body** (random, URL-safe):
      ```typescript
      import crypto from 'crypto';
      
      function generateAPIKey(prefix: string): string {
        const randomBytes = crypto.randomBytes(32);
        const key = randomBytes.toString('base64url');
        return `${prefix}${key}`;
      }
      
      const liveKey = generateAPIKey('sk_live_');
      // Example output: sk_live_[random_32_byte_base64url_string]
      ```
      
      ### Storage
      
      **Hash keys before storage**:
      ```typescript
      import bcrypt from 'bcrypt';
      
      async function createAPIKey(userId: string): Promise<string> {
        const key = generateAPIKey('sk_live_');
        const hashedKey = await bcrypt.hash(key, 10);
      
        await db.apiKeys.create({
          data: {
            userId,
            keyHash: hashedKey,
            keyPrefix: key.substring(0, 15), // Store prefix for identification
            createdAt: new Date(),
          },
        });
      
        // Return unhashed key ONCE (user must store it)
        return key;
      }
      
      async function validateAPIKey(key: string): Promise<User | null> {
        const prefix = key.substring(0, 15);
      
        const apiKey = await db.apiKeys.findFirst({
          where: { keyPrefix: prefix },
          include: { user: true },
        });
      
        if (!apiKey) {
          return null;
        }
      
        const isValid = await bcrypt.compare(key, apiKey.keyHash);
      
        if (!isValid) {
          return null;
        }
      
        return apiKey.user;
      }
      ```
      
      ### Key Rotation
      
      **Support multiple active keys**:
      ```typescript
      async function rotateAPIKey(oldKey: string): Promise<string> {
        const user = await validateAPIKey(oldKey);
      
        if (!user) {
          throw new Error('Invalid API key');
        }
      
        // Create new key
        const newKey = await createAPIKey(user.id);
      
        // Mark old key as deprecated (don't delete immediately)
        await db.apiKeys.update({
          where: { keyPrefix: oldKey.substring(0, 15) },
          data: {
            deprecated: true,
            deprecatedAt: new Date(),
            expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days grace period
          },
        });
      
        return newKey;
      }
      ```
      
      ### Key Scopes
      
      ```typescript
      interface APIKey {
        id: string;
        userId: string;
        keyHash: string;
        scopes: string[];  // ["read:users", "write:orders"]
        expiresAt: Date | null;
      }
      
      function requireAPIKeyScope(scope: string) {
        return async (req, res, next) => {
          const apiKey = req.apiKey; // Set by auth middleware
      
          if (!apiKey.scopes.includes(scope)) {
            return res.status(403).json({
              error: 'insufficient_scope',
              message: `This API key does not have the ${scope} scope`,
            });
          }
      
          next();
        };
      }
      
      app.get('/users', requireAPIKeyScope('read:users'), async (req, res) => {
        // Handler
      });
      ```
      
      ### Usage
      
      **Query parameter** (less secure, convenient for testing):
      ```http
      GET /v1/users?api_key=sk_live_abc123
      ```
      
      **Header** (recommended):
      ```http
      GET /v1/users
      Authorization: Bearer sk_live_abc123
      ```
      
      Or custom header:
      ```http
      GET /v1/users
      X-API-Key: sk_live_abc123
      ```
      
      ## Role-Based Access Control (RBAC)
      
      ### Roles and Permissions
      
      ```typescript
      interface Role {
        id: string;
        name: string;  // "admin", "editor", "viewer"
        permissions: Permission[];
      }
      
      interface Permission {
        id: string;
        resource: string;  // "users", "orders", "products"
        action: string;    // "read", "write", "delete"
      }
      
      // Example roles
      const roles = {
        admin: {
          name: 'admin',
          permissions: [
            { resource: '*', action: '*' },  // All permissions
          ],
        },
        editor: {
          name: 'editor',
          permissions: [
            { resource: 'users', action: 'read' },
            { resource: 'users', action: 'write' },
            { resource: 'posts', action: 'read' },
            { resource: 'posts', action: 'write' },
            { resource: 'posts', action: 'delete' },
          ],
        },
        viewer: {
          name: 'viewer',
          permissions: [
            { resource: 'users', action: 'read' },
            { resource: 'posts', action: 'read' },
          ],
        },
      };
      ```
      
      ### Permission Checking
      
      ```typescript
      function hasPermission(
        user: User,
        resource: string,
        action: string
      ): boolean {
        const role = roles[user.role];
      
        return role.permissions.some(
          (perm) =>
            (perm.resource === '*' || perm.resource === resource) &&
            (perm.action === '*' || perm.action === action)
        );
      }
      
      function requirePermission(resource: string, action: string) {
        return (req, res, next) => {
          if (!hasPermission(req.user, resource, action)) {
            return res.status(403).json({
              error: 'forbidden',
              message: `Requires ${action} permission on ${resource}`,
            });
          }
      
          next();
        };
      }
      
      app.delete('/users/:id', requirePermission('users', 'delete'), async (req, res) => {
        // Handler
      });
      ```
      
      ### Attribute-Based Access Control (ABAC)
      
      **More granular**: Check user attributes, resource attributes, context
      
      ```typescript
      interface Policy {
        resource: string;
        action: string;
        condition: (context: AccessContext) => boolean;
      }
      
      interface AccessContext {
        user: User;
        resource: any;
        environment: {
          ip: string;
          time: Date;
        };
      }
      
      const policies: Policy[] = [
        {
          resource: 'users',
          action: 'delete',
          condition: (ctx) =>
            ctx.user.role === 'admin' ||
            (ctx.user.role === 'editor' && ctx.resource.createdBy === ctx.user.id),
        },
        {
          resource: 'posts',
          action: 'write',
          condition: (ctx) =>
            ctx.user.role === 'admin' ||
            ctx.user.role === 'editor' ||
            (ctx.user.role === 'author' && ctx.resource.authorId === ctx.user.id),
        },
      ];
      
      function checkAccess(context: AccessContext, resource: string, action: string): boolean {
        const policy = policies.find(
          (p) => p.resource === resource && p.action === action
        );
      
        if (!policy) {
          return false;
        }
      
        return policy.condition(context);
      }
      ```
      
      ## Security Best Practices
      
      ### HTTPS/TLS Only
      
      ```typescript
      // Redirect HTTP to HTTPS
      app.use((req, res, next) => {
        if (!req.secure && process.env.NODE_ENV === 'production') {
          return res.redirect(`https://${req.hostname}${req.url}`);
        }
        next();
      });
      
      // Strict-Transport-Security header
      app.use((req, res, next) => {
        res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        next();
      });
      ```
      
      ### Rate Limiting Per User
      
      ```typescript
      import rateLimit from 'express-rate-limit';
      
      const limiter = rateLimit({
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 100, // Limit each user to 100 requests per windowMs
        keyGenerator: (req) => req.user?.id || req.ip,
        handler: (req, res) => {
          res.status(429).json({
            error: 'too_many_requests',
            message: 'Rate limit exceeded. Try again later.',
          });
        },
      });
      
      app.use('/api/', limiter);
      ```
      
      ### Token Blacklisting
      
      ```typescript
      // Blacklist JWT on logout
      app.post('/logout', async (req, res) => {
        const token = req.headers.authorization?.split(' ')[1];
        const payload = jwt.decode(token);
      
        // Store token ID in blacklist until expiration
        await redis.setex(
          `blacklist:${payload.jti}`,
          payload.exp - Math.floor(Date.now() / 1000),
          '1'
        );
      
        res.json({ message: 'Logged out successfully' });
      });
      
      // Check blacklist
      app.use(async (req, res, next) => {
        const token = req.headers.authorization?.split(' ')[1];
        const payload = jwt.decode(token);
      
        const isBlacklisted = await redis.get(`blacklist:${payload.jti}`);
      
        if (isBlacklisted) {
          return res.status(401).json({ error: 'Token has been revoked' });
        }
      
        next();
      });
      ```
      
      ### IP Whitelisting
      
      ```typescript
      const allowedIPs = ['192.168.1.1', '10.0.0.0/8'];
      
      function ipWhitelist(req, res, next) {
        const clientIP = req.ip || req.connection.remoteAddress;
      
        if (!isIPAllowed(clientIP, allowedIPs)) {
          return res.status(403).json({
            error: 'forbidden',
            message: 'Access denied from this IP address',
          });
        }
      
        next();
      }
      
      app.use('/admin', ipWhitelist);
      ```
      
      ### CORS Configuration
      
      ```typescript
      import cors from 'cors';
      
      app.use(cors({
        origin: (origin, callback) => {
          const allowedOrigins = [
            'https://app.example.com',
            'https://dashboard.example.com',
          ];
      
          if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
          } else {
            callback(new Error('Not allowed by CORS'));
          }
        },
        credentials: true,
        maxAge: 86400,
      }));
      ```
      
      ## Best Practices Summary
      
      ✅ **Use OAuth 2.0 for third-party access**: Industry standard, secure delegation
      ✅ **Use JWT for stateless auth**: Microservices, mobile apps
      ✅ **Use API keys for service-to-service**: Simple, revocable
      ✅ **Always use HTTPS**: Encrypt all traffic
      ✅ **Hash API keys before storage**: bcrypt or scrypt
      ✅ **Use short-lived tokens**: 15 min - 1 hour for access tokens
      ✅ **Implement refresh tokens**: Avoid long-lived access tokens
      ✅ **Use asymmetric JWT signing**: RS256, not HS256
      ✅ **Validate all JWT claims**: iss, aud, exp, nbf
      ✅ **Implement rate limiting**: Per user, per IP
      ✅ **Use PKCE for mobile/SPA**: Prevents code interception
      ✅ **Support token revocation**: Blacklist or database check
      ✅ **Implement RBAC or ABAC**: Fine-grained permissions
      
      ❌ **Don't use Implicit Flow**: Use Authorization Code + PKCE
      ❌ **Don't use Resource Owner Password**: Use Authorization Code
      ❌ **Don't store passwords in JWT**: JWTs are not encrypted
      ❌ **Don't use long-lived JWTs**: Hard to revoke
      ❌ **Don't skip signature verification**: Always verify JWTs
      ❌ **Don't expose tokens in URLs**: Use headers
      ❌ **Don't reuse API keys**: Rotate compromised keys
      ❌ **Don't skip HTTPS**: Production must use TLS
      
      ## Additional Resources
      
      - [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
      - [OAuth 2.0 Security Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-security-topics)
      - [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
      - [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636)
      - [OAuth 2.0 Playground](https://www.oauth.com/playground/)
      - [JWT.io Debugger](https://jwt.io/)
      
    • graphql-patterns.md 24.3 KB
      # GraphQL Patterns - Deep Dive
      
      Advanced GraphQL schema design, resolver optimization, subscriptions, federation, and production best practices.
      
      ## Schema Design Best Practices
      
      ### Type System Fundamentals
      
      **Scalar Types**:
      ```graphql
      type User {
        id: ID!              # Unique identifier
        email: String!       # Required string
        age: Int             # Optional integer
        balance: Float       # Optional float
        isActive: Boolean!   # Required boolean
      }
      ```
      
      **Custom Scalars**:
      ```graphql
      scalar DateTime
      scalar URL
      scalar EmailAddress
      scalar JSON
      
      type Post {
        id: ID!
        publishedAt: DateTime!
        website: URL
        content: JSON
      }
      ```
      
      **Implementation** (GraphQL Scalars library):
      ```typescript
      import { DateTimeResolver, URLResolver, EmailAddressResolver, JSONResolver } from 'graphql-scalars';
      
      const resolvers = {
        DateTime: DateTimeResolver,
        URL: URLResolver,
        EmailAddress: EmailAddressResolver,
        JSON: JSONResolver,
      };
      ```
      
      ### Object Types and Interfaces
      
      **Interface** (shared fields):
      ```graphql
      interface Node {
        id: ID!
        createdAt: DateTime!
        updatedAt: DateTime!
      }
      
      type User implements Node {
        id: ID!
        createdAt: DateTime!
        updatedAt: DateTime!
        email: String!
        name: String
      }
      
      type Post implements Node {
        id: ID!
        createdAt: DateTime!
        updatedAt: DateTime!
        title: String!
        content: String!
        author: User!
      }
      ```
      
      **Query interface implementations**:
      ```graphql
      query {
        node(id: "123") {
          id
          ... on User {
            email
            name
          }
          ... on Post {
            title
            author { name }
          }
        }
      }
      ```
      
      ### Union Types
      
      ```graphql
      union SearchResult = User | Post | Comment
      
      type Query {
        search(query: String!): [SearchResult!]!
      }
      ```
      
      **Query with fragments**:
      ```graphql
      query {
        search(query: "graphql") {
          ... on User {
            id
            name
            email
          }
          ... on Post {
            id
            title
            content
          }
          ... on Comment {
            id
            text
            author { name }
          }
        }
      }
      ```
      
      ### Enums
      
      ```graphql
      enum UserRole {
        ADMIN
        MODERATOR
        USER
        GUEST
      }
      
      enum OrderStatus {
        PENDING
        PROCESSING
        SHIPPED
        DELIVERED
        CANCELLED
      }
      
      type User {
        id: ID!
        role: UserRole!
        orders: [Order!]!
      }
      
      type Order {
        id: ID!
        status: OrderStatus!
      }
      ```
      
      ### Input Types
      
      ```graphql
      input CreateUserInput {
        email: String!
        name: String
        role: UserRole = USER  # Default value
      }
      
      input UpdateUserInput {
        email: String
        name: String
        role: UserRole
      }
      
      input UserFilterInput {
        role: UserRole
        isActive: Boolean
        createdAfter: DateTime
      }
      
      type Mutation {
        createUser(input: CreateUserInput!): User!
        updateUser(id: ID!, input: UpdateUserInput!): User!
      }
      
      type Query {
        users(filter: UserFilterInput): [User!]!
      }
      ```
      
      ### Nullable vs Non-Null Fields
      
      **Design philosophy**: Nullable by default, non-null where guaranteed
      
      ✅ **Good: Defensive nullability**
      ```graphql
      type User {
        id: ID!                 # Always present
        email: String!          # Required, validated
        name: String            # Optional (nullable)
        profile: Profile        # May not exist
        posts: [Post!]!         # Array never null, posts never null
        # Can return empty array []
      }
      ```
      
      ❌ **Bad: Over-promising with non-null**
      ```graphql
      type User {
        id: ID!
        email: String!
        lastLoginAt: DateTime!  # What if never logged in?
        favoritePost: Post!     # What if no favorite?
        # Breaking change if needs to be nullable later
      }
      ```
      
      **Nullability rules**:
      - `String`: Nullable string
      - `String!`: Non-null string
      - `[String]`: Nullable array of nullable strings
      - `[String!]`: Nullable array of non-null strings
      - `[String!]!`: Non-null array of non-null strings
      - `[String]!`: Non-null array of nullable strings
      
      ## Resolver Patterns
      
      ### Basic Resolvers
      
      ```typescript
      const resolvers = {
        Query: {
          user: async (_parent, { id }, context) => {
            return context.db.users.findUnique({ where: { id } });
          },
          users: async (_parent, { filter }, context) => {
            return context.db.users.findMany({ where: filter });
          },
        },
      
        User: {
          // Field resolver (computed field)
          fullName: (user) => `${user.firstName} ${user.lastName}`,
      
          // Async field resolver (database fetch)
          posts: async (user, _args, context) => {
            return context.db.posts.findMany({ where: { authorId: user.id } });
          },
        },
      
        Mutation: {
          createUser: async (_parent, { input }, context) => {
            return context.db.users.create({ data: input });
          },
        },
      };
      ```
      
      ### DataLoader (N+1 Solution)
      
      **Problem**: N+1 query pattern
      ```typescript
      // BAD: Triggers separate query for each user's posts
      const resolvers = {
        User: {
          posts: async (user, _args, context) => {
            // Called once PER user in result set
            return context.db.posts.findMany({ where: { authorId: user.id } });
          },
        },
      };
      
      // Query for 100 users = 1 query + 100 queries for posts = 101 queries!
      ```
      
      **Solution**: DataLoader batches requests
      ```typescript
      import DataLoader from 'dataloader';
      
      // Create loader in context (per-request)
      const createLoaders = (db) => ({
        postsLoader: new DataLoader(async (userIds: string[]) => {
          // Single query for all users
          const posts = await db.posts.findMany({
            where: { authorId: { in: userIds } },
          });
      
          // Group by userId
          const postsByUser = userIds.map(userId =>
            posts.filter(post => post.authorId === userId)
          );
      
          return postsByUser;
        }),
      
        userLoader: new DataLoader(async (userIds: string[]) => {
          const users = await db.users.findMany({
            where: { id: { in: userIds } },
          });
      
          // Maintain order matching userIds
          return userIds.map(id => users.find(user => user.id === id));
        }),
      });
      
      // Context setup
      const context = ({ req }) => ({
        db: prisma,
        loaders: createLoaders(prisma),
        userId: req.userId,
      });
      
      // Resolver using DataLoader
      const resolvers = {
        User: {
          posts: (user, _args, context) => {
            return context.loaders.postsLoader.load(user.id);
          },
        },
        Post: {
          author: (post, _args, context) => {
            return context.loaders.userLoader.load(post.authorId);
          },
        },
      };
      
      // Query for 100 users = 1 query + 1 batched query for posts = 2 queries!
      ```
      
      ### Resolver Chain and Parent
      
      ```typescript
      const resolvers = {
        Query: {
          user: async (_parent, { id }, context) => {
            // Returns user object passed to User resolvers
            return context.db.users.findUnique({ where: { id } });
          },
        },
      
        User: {
          // parent is the user object from Query.user
          fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
      
          // Can access parent fields
          posts: async (parent, _args, context) => {
            return context.db.posts.findMany({
              where: { authorId: parent.id },
            });
          },
      
          // Nested resolver chain
          profile: async (parent, _args, context) => {
            // Returns profile object passed to Profile resolvers
            return context.db.profiles.findUnique({
              where: { userId: parent.id },
            });
          },
        },
      
        Profile: {
          // parent is the profile object from User.profile
          avatarUrl: (parent) => {
            return parent.avatar
              ? `https://cdn.example.com/${parent.avatar}`
              : 'https://cdn.example.com/default-avatar.png';
          },
        },
      };
      ```
      
      ### Error Handling
      
      ```typescript
      import { GraphQLError } from 'graphql';
      
      const resolvers = {
        Query: {
          user: async (_parent, { id }, context) => {
            const user = await context.db.users.findUnique({ where: { id } });
      
            if (!user) {
              throw new GraphQLError('User not found', {
                extensions: {
                  code: 'NOT_FOUND',
                  argumentName: 'id',
                },
              });
            }
      
            return user;
          },
        },
      
        Mutation: {
          createUser: async (_parent, { input }, context) => {
            // Validation error
            if (!input.email.includes('@')) {
              throw new GraphQLError('Invalid email format', {
                extensions: {
                  code: 'INVALID_INPUT',
                  field: 'email',
                },
              });
            }
      
            try {
              return await context.db.users.create({ data: input });
            } catch (error) {
              // Database unique constraint
              if (error.code === 'P2002') {
                throw new GraphQLError('Email already exists', {
                  extensions: {
                    code: 'DUPLICATE_EMAIL',
                    field: 'email',
                  },
                });
              }
      
              // Unexpected error
              throw new GraphQLError('Failed to create user', {
                extensions: {
                  code: 'INTERNAL_ERROR',
                },
              });
            }
          },
        },
      };
      ```
      
      **Error response**:
      ```json
      {
        "errors": [
          {
            "message": "User not found",
            "locations": [{ "line": 2, "column": 3 }],
            "path": ["user"],
            "extensions": {
              "code": "NOT_FOUND",
              "argumentName": "id"
            }
          }
        ],
        "data": {
          "user": null
        }
      }
      ```
      
      ## Pagination Patterns
      
      ### Offset Pagination (Simple)
      
      ```graphql
      type Query {
        users(limit: Int = 10, offset: Int = 0): UserConnection!
      }
      
      type UserConnection {
        nodes: [User!]!
        totalCount: Int!
        pageInfo: PageInfo!
      }
      
      type PageInfo {
        hasNextPage: Boolean!
        hasPreviousPage: Boolean!
      }
      ```
      
      **Resolver**:
      ```typescript
      const resolvers = {
        Query: {
          users: async (_parent, { limit, offset }, context) => {
            const [nodes, totalCount] = await Promise.all([
              context.db.users.findMany({ take: limit, skip: offset }),
              context.db.users.count(),
            ]);
      
            return {
              nodes,
              totalCount,
              pageInfo: {
                hasNextPage: offset + limit < totalCount,
                hasPreviousPage: offset > 0,
              },
            };
          },
        },
      };
      ```
      
      ### Cursor Pagination (Relay Connection)
      
      **Schema**:
      ```graphql
      type Query {
        users(first: Int, after: String, last: Int, before: String): UserConnection!
      }
      
      type UserConnection {
        edges: [UserEdge!]!
        nodes: [User!]!
        pageInfo: PageInfo!
        totalCount: Int!
      }
      
      type UserEdge {
        cursor: String!
        node: User!
      }
      
      type PageInfo {
        hasNextPage: Boolean!
        hasPreviousPage: Boolean!
        startCursor: String
        endCursor: String
      }
      ```
      
      **Resolver** (using graphql-relay):
      ```typescript
      import { connectionFromArraySlice, cursorToOffset } from 'graphql-relay';
      
      const resolvers = {
        Query: {
          users: async (_parent, args, context) => {
            const { first, after, last, before } = args;
      
            // Decode cursors to offsets
            const afterOffset = after ? cursorToOffset(after) + 1 : 0;
            const beforeOffset = before ? cursorToOffset(before) : undefined;
      
            // Calculate limit and offset
            const limit = first || last || 10;
            const offset = afterOffset;
      
            // Fetch data
            const [users, totalCount] = await Promise.all([
              context.db.users.findMany({
                take: limit + 1, // Fetch one extra to check hasNextPage
                skip: offset,
              }),
              context.db.users.count(),
            ]);
      
            // Build connection
            const hasNextPage = users.length > limit;
            const nodes = hasNextPage ? users.slice(0, -1) : users;
      
            return connectionFromArraySlice(nodes, args, {
              sliceStart: offset,
              arrayLength: totalCount,
            });
          },
        },
      };
      ```
      
      **Query**:
      ```graphql
      query {
        users(first: 10, after: "cursor123") {
          edges {
            cursor
            node {
              id
              name
            }
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
      ```
      
      ## Mutations
      
      ### Input Object Pattern
      
      ✅ **Good: Single input object**
      ```graphql
      input CreatePostInput {
        title: String!
        content: String!
        tags: [String!]
        publishedAt: DateTime
      }
      
      type Mutation {
        createPost(input: CreatePostInput!): CreatePostPayload!
      }
      ```
      
      ❌ **Bad: Multiple arguments**
      ```graphql
      type Mutation {
        createPost(
          title: String!
          content: String!
          tags: [String!]
          publishedAt: DateTime
        ): Post!
      }
      ```
      
      ### Payload Object Pattern
      
      **Include user errors and edge for optimistic updates**:
      ```graphql
      type CreatePostPayload {
        post: Post
        postEdge: PostEdge
        errors: [UserError!]
        clientMutationId: String
      }
      
      type UserError {
        message: String!
        field: String
        code: String!
      }
      
      type PostEdge {
        cursor: String!
        node: Post!
      }
      
      type Mutation {
        createPost(input: CreatePostInput!): CreatePostPayload!
      }
      ```
      
      **Resolver**:
      ```typescript
      const resolvers = {
        Mutation: {
          createPost: async (_parent, { input }, context) => {
            // Validation
            const errors = [];
            if (input.title.length < 3) {
              errors.push({
                message: 'Title must be at least 3 characters',
                field: 'title',
                code: 'TITLE_TOO_SHORT',
              });
            }
      
            if (errors.length > 0) {
              return { post: null, postEdge: null, errors };
            }
      
            // Create post
            const post = await context.db.posts.create({
              data: {
                ...input,
                authorId: context.userId,
              },
            });
      
            return {
              post,
              postEdge: {
                cursor: encodeCursor(post.id),
                node: post,
              },
              errors: [],
            };
          },
        },
      };
      ```
      
      ### Optimistic Updates (Client)
      
      ```typescript
      const [createPost] = useMutation(CREATE_POST, {
        optimisticResponse: {
          createPost: {
            __typename: 'CreatePostPayload',
            post: {
              __typename: 'Post',
              id: 'temp-id',
              title: variables.input.title,
              content: variables.input.content,
              createdAt: new Date().toISOString(),
            },
            errors: [],
          },
        },
        update: (cache, { data }) => {
          // Update cache with new post
          const existing = cache.readQuery({ query: GET_POSTS });
          cache.writeQuery({
            query: GET_POSTS,
            data: {
              posts: {
                ...existing.posts,
                edges: [
                  data.createPost.postEdge,
                  ...existing.posts.edges,
                ],
              },
            },
          });
        },
      });
      ```
      
      ## Subscriptions (Real-Time)
      
      ### Schema
      
      ```graphql
      type Subscription {
        postAdded: Post!
        postUpdated(id: ID!): Post!
        commentAdded(postId: ID!): Comment!
        userStatusChanged(userId: ID!): UserStatus!
      }
      
      type UserStatus {
        userId: ID!
        isOnline: Boolean!
        lastSeen: DateTime
      }
      ```
      
      ### Resolver (with PubSub)
      
      ```typescript
      import { PubSub } from 'graphql-subscriptions';
      
      const pubsub = new PubSub();
      
      const resolvers = {
        Subscription: {
          postAdded: {
            subscribe: () => pubsub.asyncIterator(['POST_ADDED']),
          },
      
          postUpdated: {
            subscribe: (_parent, { id }) => {
              return pubsub.asyncIterator([`POST_UPDATED_${id}`]);
            },
          },
      
          commentAdded: {
            subscribe: (_parent, { postId }, context) => {
              // Auth check
              if (!context.userId) {
                throw new GraphQLError('Unauthorized');
              }
              return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
            },
            // Optional resolve function
            resolve: (payload) => payload.comment,
          },
        },
      
        Mutation: {
          createPost: async (_parent, { input }, context) => {
            const post = await context.db.posts.create({ data: input });
      
            // Trigger subscription
            pubsub.publish('POST_ADDED', { postAdded: post });
      
            return { post };
          },
      
          updatePost: async (_parent, { id, input }, context) => {
            const post = await context.db.posts.update({
              where: { id },
              data: input,
            });
      
            pubsub.publish(`POST_UPDATED_${id}`, { postUpdated: post });
      
            return { post };
          },
        },
      };
      ```
      
      ### Redis PubSub (Production)
      
      ```typescript
      import { RedisPubSub } from 'graphql-redis-subscriptions';
      import Redis from 'ioredis';
      
      const options = {
        host: process.env.REDIS_HOST,
        port: process.env.REDIS_PORT,
        retryStrategy: (times) => Math.min(times * 50, 2000),
      };
      
      const pubsub = new RedisPubSub({
        publisher: new Redis(options),
        subscriber: new Redis(options),
      });
      ```
      
      ### WebSocket Setup (Apollo Server)
      
      ```typescript
      import { ApolloServer } from '@apollo/server';
      import { expressMiddleware } from '@apollo/server/express4';
      import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
      import { createServer } from 'http';
      import { WebSocketServer } from 'ws';
      import { useServer } from 'graphql-ws/lib/use/ws';
      import { makeExecutableSchema } from '@graphql-tools/schema';
      import express from 'express';
      
      const schema = makeExecutableSchema({ typeDefs, resolvers });
      const app = express();
      const httpServer = createServer(app);
      
      // WebSocket server
      const wsServer = new WebSocketServer({
        server: httpServer,
        path: '/graphql',
      });
      
      const serverCleanup = useServer({ schema }, wsServer);
      
      // Apollo Server
      const server = new ApolloServer({
        schema,
        plugins: [
          ApolloServerPluginDrainHttpServer({ httpServer }),
          {
            async serverWillStart() {
              return {
                async drainServer() {
                  await serverCleanup.dispose();
                },
              };
            },
          },
        ],
      });
      
      await server.start();
      
      app.use('/graphql', express.json(), expressMiddleware(server));
      
      httpServer.listen(4000);
      ```
      
      ### Client Subscription (Apollo Client)
      
      ```typescript
      import { useSubscription } from '@apollo/client';
      
      const POST_ADDED = gql`
        subscription OnPostAdded {
          postAdded {
            id
            title
            author { name }
          }
        }
      `;
      
      function RecentPosts() {
        const { data, loading } = useSubscription(POST_ADDED, {
          onData: ({ client, data }) => {
            // Update cache
            client.cache.modify({
              fields: {
                posts: (existing) => ({
                  ...existing,
                  edges: [
                    { node: data.postAdded, cursor: '' },
                    ...existing.edges,
                  ],
                }),
              },
            });
          },
        });
      
        return <div>New post: {data?.postAdded.title}</div>;
      }
      ```
      
      ## Directives
      
      ### Built-in Directives
      
      ```graphql
      query GetUser($includeEmail: Boolean!, $skipProfile: Boolean!) {
        user(id: "123") {
          id
          name
          email @include(if: $includeEmail)
          profile @skip(if: $skipProfile) {
            bio
          }
        }
      }
      ```
      
      ### Custom Directives
      
      **Schema**:
      ```graphql
      directive @auth(requires: UserRole!) on FIELD_DEFINITION
      directive @deprecated(reason: String) on FIELD_DEFINITION
      directive @length(min: Int, max: Int) on INPUT_FIELD_DEFINITION
      
      type Query {
        users: [User!]! @auth(requires: ADMIN)
        me: User!
      }
      
      type User {
        id: ID!
        email: String! @deprecated(reason: "Use contactEmail instead")
        contactEmail: String!
      }
      
      input CreateUserInput {
        name: String! @length(min: 3, max: 50)
        email: String!
      }
      ```
      
      **Implementation** (using graphql-tools):
      ```typescript
      import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
      
      function authDirective(schema, directiveName) {
        return mapSchema(schema, {
          [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
      
            if (authDirective) {
              const { requires } = authDirective;
              const { resolve = defaultFieldResolver } = fieldConfig;
      
              fieldConfig.resolve = async (source, args, context, info) => {
                if (!context.user || context.user.role !== requires) {
                  throw new GraphQLError('Unauthorized', {
                    extensions: { code: 'FORBIDDEN' },
                  });
                }
      
                return resolve(source, args, context, info);
              };
            }
      
            return fieldConfig;
          },
        });
      }
      
      let schema = makeExecutableSchema({ typeDefs, resolvers });
      schema = authDirective(schema, 'auth');
      ```
      
      ## Performance Optimization
      
      ### Query Complexity Analysis
      
      ```typescript
      import { createComplexityLimitRule } from 'graphql-validation-complexity';
      
      const server = new ApolloServer({
        schema,
        validationRules: [
          createComplexityLimitRule(1000, {
            scalarCost: 1,
            objectCost: 10,
            listFactor: 10,
            introspectionListFactor: 10,
            onCost: (cost) => {
              console.log('Query cost:', cost);
            },
          }),
        ],
      });
      ```
      
      **Custom cost per field**:
      ```typescript
      const typeDefs = gql`
        type Query {
          users: [User!]! @cost(complexity: 100)
          expensiveAnalytics: Analytics! @cost(complexity: 500)
        }
      `;
      ```
      
      ### Query Depth Limiting
      
      ```typescript
      import depthLimit from 'graphql-depth-limit';
      
      const server = new ApolloServer({
        schema,
        validationRules: [depthLimit(10)],
      });
      ```
      
      ### Persisted Queries
      
      **Benefits**: Reduce payload size, prevent arbitrary queries in production
      
      ```typescript
      import { ApolloServer } from '@apollo/server';
      
      const server = new ApolloServer({
        schema,
        persistedQueries: {
          cache: new Map(), // Use Redis in production
        },
        allowBatchedHttpRequests: false,
        introspection: process.env.NODE_ENV !== 'production',
      });
      ```
      
      **Client sends hash**:
      ```http
      POST /graphql
      {
        "extensions": {
          "persistedQuery": {
            "version": 1,
            "sha256Hash": "abc123..."
          }
        }
      }
      ```
      
      ### Response Caching
      
      **HTTP caching**:
      ```typescript
      import responseCachePlugin from '@apollo/server-plugin-response-cache';
      
      const server = new ApolloServer({
        schema,
        plugins: [
          responseCachePlugin({
            sessionId: (context) => context.user?.id || null,
          }),
        ],
      });
      ```
      
      **Cache hints**:
      ```graphql
      type Query {
        user(id: ID!): User @cacheControl(maxAge: 60, scope: PRIVATE)
        publicPosts: [Post!]! @cacheControl(maxAge: 300, scope: PUBLIC)
      }
      ```
      
      ## Schema Stitching and Federation
      
      ### Apollo Federation
      
      **Service 1 (Users)**:
      ```graphql
      type User @key(fields: "id") {
        id: ID!
        email: String!
        name: String
      }
      
      extend type Query {
        user(id: ID!): User
      }
      ```
      
      **Service 2 (Posts)**:
      ```graphql
      type Post @key(fields: "id") {
        id: ID!
        title: String!
        author: User!
      }
      
      extend type User @key(fields: "id") {
        id: ID! @external
        posts: [Post!]!
      }
      
      extend type Query {
        posts: [Post!]!
      }
      ```
      
      **Gateway**:
      ```typescript
      import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
      import { ApolloServer } from '@apollo/server';
      
      const gateway = new ApolloGateway({
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://localhost:4001/graphql' },
            { name: 'posts', url: 'http://localhost:4002/graphql' },
          ],
        }),
      });
      
      const server = new ApolloServer({ gateway });
      ```
      
      ## Testing
      
      ### Unit Testing Resolvers
      
      ```typescript
      import { resolvers } from './resolvers';
      
      describe('User Resolvers', () => {
        it('fetches user by ID', async () => {
          const mockDb = {
            users: {
              findUnique: jest.fn().mockResolvedValue({
                id: '123',
                email: 'test@example.com',
              }),
            },
          };
      
          const result = await resolvers.Query.user(
            {},
            { id: '123' },
            { db: mockDb }
          );
      
          expect(mockDb.users.findUnique).toHaveBeenCalledWith({
            where: { id: '123' },
          });
          expect(result).toEqual({
            id: '123',
            email: 'test@example.com',
          });
        });
      });
      ```
      
      ### Integration Testing
      
      ```typescript
      import { ApolloServer } from '@apollo/server';
      
      const server = new ApolloServer({ typeDefs, resolvers });
      
      it('creates a user', async () => {
        const response = await server.executeOperation({
          query: `
            mutation CreateUser($input: CreateUserInput!) {
              createUser(input: $input) {
                user {
                  id
                  email
                }
                errors {
                  message
                }
              }
            }
          `,
          variables: {
            input: {
              email: 'test@example.com',
              name: 'Test User',
            },
          },
        });
      
        expect(response.body.kind).toBe('single');
        expect(response.body.singleResult.errors).toBeUndefined();
        expect(response.body.singleResult.data?.createUser.user).toHaveProperty('id');
      });
      ```
      
      ## Best Practices Summary
      
      ✅ **Nullable by default**: Only use non-null (`!`) when guaranteed
      ✅ **Use DataLoader**: Batch queries to prevent N+1
      ✅ **Pagination**: Use cursor-based for large lists
      ✅ **Input objects**: Group mutation arguments
      ✅ **Payload objects**: Return errors with data
      ✅ **Custom scalars**: Use DateTime, Email, URL, JSON
      ✅ **Interfaces**: Share common fields across types
      ✅ **Query complexity**: Limit expensive queries
      ✅ **Persisted queries**: Reduce payload, improve security
      ✅ **Error handling**: Return specific error codes and fields
      
      ❌ **Avoid over-fetching**: Let clients request exact fields
      ❌ **Don't expose internal IDs**: Use opaque IDs or UUIDs
      ❌ **Don't ignore N+1**: Always use DataLoader for relationships
      ❌ **Don't make everything non-null**: Breaks schema evolution
      ❌ **Don't use query strings for mutations**: Use input objects
      ❌ **Don't skip authorization**: Check permissions in resolvers
      
      ## Additional Resources
      
      - [GraphQL Official Documentation](https://graphql.org/learn/)
      - [Apollo Server Documentation](https://www.apollographql.com/docs/apollo-server/)
      - [GraphQL Best Practices](https://graphql.org/learn/best-practices/)
      - [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
      - [Apollo Federation](https://www.apollographql.com/docs/federation/)
      - [DataLoader Documentation](https://github.com/graphql/dataloader)
      
    • grpc-patterns.md 27.4 KB
      # gRPC Patterns - Deep Dive
      
      Comprehensive gRPC service design, streaming patterns, error handling, interceptors, and production deployment strategies.
      
      ## Protocol Buffers (Protobuf)
      
      ### Basic Message Definition
      
      ```protobuf
      syntax = "proto3";
      
      package users.v1;
      
      // Import well-known types
      import "google/protobuf/timestamp.proto";
      import "google/protobuf/empty.proto";
      
      // User message
      message User {
        string id = 1;
        string email = 2;
        string name = 3;
        UserRole role = 4;
        google.protobuf.Timestamp created_at = 5;
        google.protobuf.Timestamp updated_at = 6;
      }
      
      // Enum for user roles
      enum UserRole {
        USER_ROLE_UNSPECIFIED = 0;  // Required first value
        USER_ROLE_USER = 1;
        USER_ROLE_ADMIN = 2;
        USER_ROLE_MODERATOR = 3;
      }
      ```
      
      ### Field Numbering Best Practices
      
      ✅ **Good: Strategic numbering**
      ```protobuf
      message User {
        // 1-15: Single-byte encoding (most common fields)
        string id = 1;
        string email = 2;
        string name = 3;
      
        // 16-2047: Two-byte encoding (less common fields)
        string bio = 16;
        string website = 17;
      
        // 19000-19999: Reserved range (do not use)
        // 20000+: Multi-byte encoding (rare fields)
      }
      ```
      
      ❌ **Bad: Random numbering**
      ```protobuf
      message User {
        string id = 100;      // Wastes encoding space
        string email = 3;
        string name = 15000;  // Very inefficient
      }
      ```
      
      ### Nested Messages
      
      ```protobuf
      message User {
        string id = 1;
        string email = 2;
        Profile profile = 3;
      
        message Profile {
          string bio = 1;
          string avatar_url = 2;
          Address address = 3;
      
          message Address {
            string street = 1;
            string city = 2;
            string country = 3;
            string postal_code = 4;
          }
        }
      }
      ```
      
      ### Repeated Fields (Arrays)
      
      ```protobuf
      message User {
        string id = 1;
        repeated string tags = 2;           // Array of strings
        repeated Role roles = 3;            // Array of enums
        repeated Address addresses = 4;     // Array of messages
      }
      ```
      
      ### Maps
      
      ```protobuf
      message User {
        string id = 1;
        map<string, string> metadata = 2;        // String map
        map<string, int32> settings = 3;         // Mixed types
        map<string, Address> addresses = 4;      // Complex values
      }
      ```
      
      ### Oneofs (Union Types)
      
      ```protobuf
      message SearchRequest {
        string query = 1;
      
        oneof filter {
          UserFilter user_filter = 2;
          PostFilter post_filter = 3;
          CommentFilter comment_filter = 4;
        }
      }
      
      message UserFilter {
        UserRole role = 1;
        bool is_active = 2;
      }
      ```
      
      ### Reserved Fields
      
      ```protobuf
      message User {
        reserved 4, 5, 6;                    // Reserved field numbers
        reserved "old_field", "deprecated";  // Reserved field names
      
        string id = 1;
        string email = 2;
        string name = 3;
        // Fields 4-6 cannot be reused
        string new_field = 7;
      }
      ```
      
      ## Service Definition
      
      ### Unary RPC (Request/Response)
      
      ```protobuf
      service UserService {
        rpc GetUser(GetUserRequest) returns (User) {}
        rpc CreateUser(CreateUserRequest) returns (User) {}
        rpc UpdateUser(UpdateUserRequest) returns (User) {}
        rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) {}
      }
      
      message GetUserRequest {
        string id = 1;
      }
      
      message CreateUserRequest {
        string email = 1;
        string name = 2;
        UserRole role = 3;
      }
      
      message UpdateUserRequest {
        string id = 1;
        optional string email = 2;
        optional string name = 3;
        optional UserRole role = 4;
      }
      
      message DeleteUserRequest {
        string id = 1;
      }
      ```
      
      ### Server Streaming RPC
      
      Server sends multiple messages in response to single client request:
      
      ```protobuf
      service UserService {
        // Stream all users
        rpc ListUsers(ListUsersRequest) returns (stream User) {}
      
        // Stream user events
        rpc WatchUser(WatchUserRequest) returns (stream UserEvent) {}
      }
      
      message ListUsersRequest {
        int32 page_size = 1;
        string page_token = 2;
        UserRole role = 3;
      }
      
      message WatchUserRequest {
        string user_id = 1;
      }
      
      message UserEvent {
        string event_id = 1;
        EventType type = 2;
        User user = 3;
        google.protobuf.Timestamp timestamp = 4;
      }
      
      enum EventType {
        EVENT_TYPE_UNSPECIFIED = 0;
        EVENT_TYPE_CREATED = 1;
        EVENT_TYPE_UPDATED = 2;
        EVENT_TYPE_DELETED = 3;
      }
      ```
      
      ### Client Streaming RPC
      
      Client sends multiple messages, server responds once:
      
      ```protobuf
      service UserService {
        // Bulk create users
        rpc BulkCreateUsers(stream CreateUserRequest) returns (BulkCreateUsersResponse) {}
      
        // Upload user data
        rpc UploadUserData(stream UserDataChunk) returns (UploadResponse) {}
      }
      
      message BulkCreateUsersResponse {
        int32 created_count = 1;
        repeated User users = 2;
        repeated Error errors = 3;
      }
      
      message UserDataChunk {
        bytes data = 1;
        int32 chunk_number = 2;
      }
      
      message UploadResponse {
        int64 bytes_received = 1;
        string file_id = 2;
      }
      ```
      
      ### Bidirectional Streaming RPC
      
      Both client and server send multiple messages:
      
      ```protobuf
      service ChatService {
        rpc Chat(stream ChatMessage) returns (stream ChatMessage) {}
      }
      
      message ChatMessage {
        string id = 1;
        string user_id = 2;
        string text = 3;
        google.protobuf.Timestamp sent_at = 4;
      }
      ```
      
      ## Server Implementation
      
      ### Go Server
      
      ```go
      package main
      
      import (
          "context"
          "log"
          "net"
      
          "google.golang.org/grpc"
          "google.golang.org/grpc/codes"
          "google.golang.org/grpc/status"
          pb "myapp/proto/users/v1"
      )
      
      type server struct {
          pb.UnimplementedUserServiceServer
          db *Database
      }
      
      // Unary RPC
      func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
          if req.Id == "" {
              return nil, status.Error(codes.InvalidArgument, "user ID is required")
          }
      
          user, err := s.db.GetUser(ctx, req.Id)
          if err != nil {
              if errors.Is(err, sql.ErrNoRows) {
                  return nil, status.Error(codes.NotFound, "user not found")
              }
              return nil, status.Error(codes.Internal, "database error")
          }
      
          return &pb.User{
              Id:    user.ID,
              Email: user.Email,
              Name:  user.Name,
              Role:  pb.UserRole(user.Role),
          }, nil
      }
      
      // Server streaming RPC
      func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
          users, err := s.db.ListUsers(stream.Context(), req)
          if err != nil {
              return status.Error(codes.Internal, "failed to fetch users")
          }
      
          for _, user := range users {
              if err := stream.Send(&pb.User{
                  Id:    user.ID,
                  Email: user.Email,
                  Name:  user.Name,
              }); err != nil {
                  return status.Error(codes.Internal, "failed to send user")
              }
          }
      
          return nil
      }
      
      // Client streaming RPC
      func (s *server) BulkCreateUsers(stream pb.UserService_BulkCreateUsersServer) error {
          var users []*pb.User
          var errors []*pb.Error
      
          for {
              req, err := stream.Recv()
              if err == io.EOF {
                  // Client finished sending
                  return stream.SendAndClose(&pb.BulkCreateUsersResponse{
                      CreatedCount: int32(len(users)),
                      Users:        users,
                      Errors:       errors,
                  })
              }
              if err != nil {
                  return status.Error(codes.Internal, "failed to receive request")
              }
      
              user, err := s.db.CreateUser(stream.Context(), req)
              if err != nil {
                  errors = append(errors, &pb.Error{
                      Message: err.Error(),
                      Field:   "email",
                  })
                  continue
              }
      
              users = append(users, user)
          }
      }
      
      // Bidirectional streaming RPC
      func (s *server) Chat(stream pb.ChatService_ChatServer) error {
          for {
              msg, err := stream.Recv()
              if err == io.EOF {
                  return nil
              }
              if err != nil {
                  return status.Error(codes.Internal, "failed to receive message")
              }
      
              // Process message
              response := &pb.ChatMessage{
                  Id:     generateID(),
                  UserId: "bot",
                  Text:   fmt.Sprintf("Echo: %s", msg.Text),
                  SentAt: timestamppb.Now(),
              }
      
              if err := stream.Send(response); err != nil {
                  return status.Error(codes.Internal, "failed to send message")
              }
          }
      }
      
      func main() {
          lis, err := net.Listen("tcp", ":50051")
          if err != nil {
              log.Fatalf("failed to listen: %v", err)
          }
      
          s := grpc.NewServer()
          pb.RegisterUserServiceServer(s, &server{db: newDatabase()})
      
          log.Printf("server listening at %v", lis.Addr())
          if err := s.Serve(lis); err != nil {
              log.Fatalf("failed to serve: %v", err)
          }
      }
      ```
      
      ### Node.js/TypeScript Server
      
      ```typescript
      import * as grpc from '@grpc/grpc-js';
      import * as protoLoader from '@grpc/proto-loader';
      import { UserServiceHandlers } from './proto/users/v1/user_service';
      
      const packageDefinition = protoLoader.loadSync('proto/users/v1/user.proto', {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true,
      });
      
      const userProto = grpc.loadPackageDefinition(packageDefinition).users.v1;
      
      const server = new grpc.Server();
      
      const userService: UserServiceHandlers = {
        // Unary RPC
        getUser: async (call, callback) => {
          const { id } = call.request;
      
          if (!id) {
            return callback({
              code: grpc.status.INVALID_ARGUMENT,
              message: 'User ID is required',
            });
          }
      
          try {
            const user = await db.users.findUnique({ where: { id } });
      
            if (!user) {
              return callback({
                code: grpc.status.NOT_FOUND,
                message: 'User not found',
              });
            }
      
            callback(null, {
              id: user.id,
              email: user.email,
              name: user.name,
              role: user.role,
            });
          } catch (error) {
            callback({
              code: grpc.status.INTERNAL,
              message: 'Database error',
            });
          }
        },
      
        // Server streaming RPC
        listUsers: async (call) => {
          const users = await db.users.findMany();
      
          for (const user of users) {
            call.write({
              id: user.id,
              email: user.email,
              name: user.name,
            });
          }
      
          call.end();
        },
      
        // Client streaming RPC
        bulkCreateUsers: async (call, callback) => {
          const users: any[] = [];
          const errors: any[] = [];
      
          call.on('data', async (request) => {
            try {
              const user = await db.users.create({ data: request });
              users.push(user);
            } catch (error) {
              errors.push({ message: error.message, field: 'email' });
            }
          });
      
          call.on('end', () => {
            callback(null, {
              created_count: users.length,
              users,
              errors,
            });
          });
      
          call.on('error', (error) => {
            callback({
              code: grpc.status.INTERNAL,
              message: error.message,
            });
          });
        },
      
        // Bidirectional streaming RPC
        chat: (call) => {
          call.on('data', (message) => {
            // Echo message back
            call.write({
              id: generateId(),
              user_id: 'bot',
              text: `Echo: ${message.text}`,
              sent_at: new Date(),
            });
          });
      
          call.on('end', () => {
            call.end();
          });
        },
      };
      
      server.addService(userProto.UserService.service, userService);
      
      server.bindAsync(
        '0.0.0.0:50051',
        grpc.ServerCredentials.createInsecure(),
        (err, port) => {
          if (err) {
            console.error('Failed to bind server:', err);
            return;
          }
          console.log(`Server running on port ${port}`);
          server.start();
        }
      );
      ```
      
      ## Client Implementation
      
      ### Go Client
      
      ```go
      package main
      
      import (
          "context"
          "log"
          "time"
      
          "google.golang.org/grpc"
          "google.golang.org/grpc/credentials/insecure"
          pb "myapp/proto/users/v1"
      )
      
      func main() {
          conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
          if err != nil {
              log.Fatalf("did not connect: %v", err)
          }
          defer conn.Close()
      
          client := pb.NewUserServiceClient(conn)
          ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
          defer cancel()
      
          // Unary call
          user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
          if err != nil {
              log.Fatalf("could not get user: %v", err)
          }
          log.Printf("User: %v", user)
      
          // Server streaming call
          stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 10})
          if err != nil {
              log.Fatalf("could not list users: %v", err)
          }
      
          for {
              user, err := stream.Recv()
              if err == io.EOF {
                  break
              }
              if err != nil {
                  log.Fatalf("error receiving: %v", err)
              }
              log.Printf("User: %v", user)
          }
      
          // Client streaming call
          bulkStream, err := client.BulkCreateUsers(ctx)
          if err != nil {
              log.Fatalf("could not create bulk stream: %v", err)
          }
      
          users := []*pb.CreateUserRequest{
              {Email: "alice@example.com", Name: "Alice"},
              {Email: "bob@example.com", Name: "Bob"},
          }
      
          for _, req := range users {
              if err := bulkStream.Send(req); err != nil {
                  log.Fatalf("failed to send: %v", err)
              }
          }
      
          response, err := bulkStream.CloseAndRecv()
          if err != nil {
              log.Fatalf("failed to receive response: %v", err)
          }
          log.Printf("Created %d users", response.CreatedCount)
      }
      ```
      
      ### TypeScript Client
      
      ```typescript
      import * as grpc from '@grpc/grpc-js';
      import * as protoLoader from '@grpc/proto-loader';
      
      const packageDefinition = protoLoader.loadSync('proto/users/v1/user.proto');
      const userProto = grpc.loadPackageDefinition(packageDefinition).users.v1;
      
      const client = new userProto.UserService(
        'localhost:50051',
        grpc.credentials.createInsecure()
      );
      
      // Unary call
      client.getUser({ id: '123' }, (error, response) => {
        if (error) {
          console.error('Error:', error);
          return;
        }
        console.log('User:', response);
      });
      
      // Promise wrapper for unary calls
      function getUserAsync(id: string): Promise<any> {
        return new Promise((resolve, reject) => {
          client.getUser({ id }, (error, response) => {
            if (error) reject(error);
            else resolve(response);
          });
        });
      }
      
      // Server streaming call
      const stream = client.listUsers({ page_size: 10 });
      
      stream.on('data', (user) => {
        console.log('User:', user);
      });
      
      stream.on('end', () => {
        console.log('Stream ended');
      });
      
      stream.on('error', (error) => {
        console.error('Stream error:', error);
      });
      
      // Client streaming call
      const bulkStream = client.bulkCreateUsers((error, response) => {
        if (error) {
          console.error('Error:', error);
          return;
        }
        console.log(`Created ${response.created_count} users`);
      });
      
      bulkStream.write({ email: 'alice@example.com', name: 'Alice' });
      bulkStream.write({ email: 'bob@example.com', name: 'Bob' });
      bulkStream.end();
      ```
      
      ## Error Handling
      
      ### gRPC Status Codes
      
      ```go
      import "google.golang.org/grpc/codes"
      
      // codes.OK                 - Success
      // codes.Canceled           - Operation canceled
      // codes.Unknown            - Unknown error
      // codes.InvalidArgument    - Invalid client input
      // codes.DeadlineExceeded   - Timeout
      // codes.NotFound           - Resource not found
      // codes.AlreadyExists      - Resource already exists
      // codes.PermissionDenied   - No permission
      // codes.ResourceExhausted  - Rate limit, quota
      // codes.FailedPrecondition - System state invalid
      // codes.Aborted            - Concurrency conflict
      // codes.OutOfRange         - Out of valid range
      // codes.Unimplemented      - Not implemented
      // codes.Internal           - Internal server error
      // codes.Unavailable        - Service unavailable
      // codes.DataLoss           - Data corruption
      // codes.Unauthenticated    - Invalid credentials
      ```
      
      ### Rich Error Details
      
      ```go
      import (
          "google.golang.org/genproto/googleapis/rpc/errdetails"
          "google.golang.org/grpc/status"
      )
      
      func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
          // Validation errors
          if req.Email == "" || !strings.Contains(req.Email, "@") {
              st := status.New(codes.InvalidArgument, "invalid email")
      
              br := &errdetails.BadRequest{
                  FieldViolations: []*errdetails.BadRequest_FieldViolation{
                      {
                          Field:       "email",
                          Description: "email must be valid format",
                      },
                  },
              }
      
              st, _ = st.WithDetails(br)
              return nil, st.Err()
          }
      
          // Quota/rate limit
          if !s.checkQuota(ctx) {
              st := status.New(codes.ResourceExhausted, "quota exceeded")
      
              qi := &errdetails.QuotaFailure{
                  Violations: []*errdetails.QuotaFailure_Violation{
                      {
                          Subject:     "user:" + getUserID(ctx),
                          Description: "API quota exceeded. Try again in 60 seconds",
                      },
                  },
              }
      
              st, _ = st.WithDetails(qi)
              return nil, st.Err()
          }
      
          return user, nil
      }
      ```
      
      **Client error handling**:
      ```go
      user, err := client.GetUser(ctx, req)
      if err != nil {
          st := status.Convert(err)
          log.Printf("Error code: %s", st.Code())
          log.Printf("Error message: %s", st.Message())
      
          for _, detail := range st.Details() {
              switch t := detail.(type) {
              case *errdetails.BadRequest:
                  for _, violation := range t.FieldViolations {
                      log.Printf("Field %s: %s", violation.Field, violation.Description)
                  }
              case *errdetails.QuotaFailure:
                  for _, violation := range t.Violations {
                      log.Printf("Quota: %s", violation.Description)
                  }
              }
          }
      }
      ```
      
      ## Interceptors (Middleware)
      
      ### Server Interceptor (Go)
      
      ```go
      import (
          "context"
          "log"
          "time"
      
          "google.golang.org/grpc"
      )
      
      // Unary interceptor
      func loggingInterceptor(
          ctx context.Context,
          req interface{},
          info *grpc.UnaryServerInfo,
          handler grpc.UnaryHandler,
      ) (interface{}, error) {
          start := time.Now()
      
          // Call handler
          resp, err := handler(ctx, req)
      
          duration := time.Since(start)
          log.Printf("Method: %s, Duration: %v, Error: %v", info.FullMethod, duration, err)
      
          return resp, err
      }
      
      // Stream interceptor
      func streamLoggingInterceptor(
          srv interface{},
          ss grpc.ServerStream,
          info *grpc.StreamServerInfo,
          handler grpc.StreamHandler,
      ) error {
          start := time.Now()
      
          err := handler(srv, ss)
      
          duration := time.Since(start)
          log.Printf("Stream: %s, Duration: %v, Error: %v", info.FullMethod, duration, err)
      
          return err
      }
      
      // Register interceptors
      s := grpc.NewServer(
          grpc.UnaryInterceptor(loggingInterceptor),
          grpc.StreamInterceptor(streamLoggingInterceptor),
      )
      ```
      
      ### Authentication Interceptor
      
      ```go
      func authInterceptor(
          ctx context.Context,
          req interface{},
          info *grpc.UnaryServerInfo,
          handler grpc.UnaryHandler,
      ) (interface{}, error) {
          // Extract metadata
          md, ok := metadata.FromIncomingContext(ctx)
          if !ok {
              return nil, status.Error(codes.Unauthenticated, "missing metadata")
          }
      
          // Check authorization header
          tokens := md["authorization"]
          if len(tokens) == 0 {
              return nil, status.Error(codes.Unauthenticated, "missing token")
          }
      
          token := tokens[0]
          userID, err := validateToken(token)
          if err != nil {
              return nil, status.Error(codes.Unauthenticated, "invalid token")
          }
      
          // Add user to context
          ctx = context.WithValue(ctx, "userID", userID)
      
          return handler(ctx, req)
      }
      ```
      
      ### Client Interceptor (Go)
      
      ```go
      func clientLoggingInterceptor(
          ctx context.Context,
          method string,
          req, reply interface{},
          cc *grpc.ClientConn,
          invoker grpc.UnaryInvoker,
          opts ...grpc.CallOption,
      ) error {
          start := time.Now()
      
          err := invoker(ctx, method, req, reply, cc, opts...)
      
          log.Printf("Method: %s, Duration: %v", method, time.Since(start))
      
          return err
      }
      
      // Use interceptor
      conn, err := grpc.Dial(
          "localhost:50051",
          grpc.WithUnaryInterceptor(clientLoggingInterceptor),
      )
      ```
      
      ## Metadata (Headers)
      
      ### Server: Read Metadata
      
      ```go
      import "google.golang.org/grpc/metadata"
      
      func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
          md, ok := metadata.FromIncomingContext(ctx)
          if !ok {
              return nil, status.Error(codes.InvalidArgument, "missing metadata")
          }
      
          // Get header values
          tokens := md["authorization"]
          userAgent := md["user-agent"]
      
          log.Printf("Authorization: %v", tokens)
          log.Printf("User-Agent: %v", userAgent)
      
          return user, nil
      }
      ```
      
      ### Server: Send Metadata
      
      ```go
      func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
          // Send header
          header := metadata.Pairs("x-request-id", generateID())
          grpc.SendHeader(ctx, header)
      
          // Send trailer
          trailer := metadata.Pairs("x-response-time", "123ms")
          grpc.SetTrailer(ctx, trailer)
      
          return user, nil
      }
      ```
      
      ### Client: Send Metadata
      
      ```go
      func main() {
          ctx := context.Background()
      
          // Add metadata to context
          md := metadata.New(map[string]string{
              "authorization": "Bearer token123",
              "x-request-id":  generateID(),
          })
          ctx = metadata.NewOutgoingContext(ctx, md)
      
          // Make call with metadata
          user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
      }
      ```
      
      ### Client: Receive Metadata
      
      ```go
      var header, trailer metadata.MD
      
      user, err := client.GetUser(
          ctx,
          req,
          grpc.Header(&header),
          grpc.Trailer(&trailer),
      )
      
      if err == nil {
          log.Printf("Header: %v", header)
          log.Printf("Trailer: %v", trailer)
      }
      ```
      
      ## Performance Optimization
      
      ### Connection Pooling
      
      ```go
      // Client-side connection pool
      var (
          conn     *grpc.ClientConn
          connOnce sync.Once
      )
      
      func getConnection() *grpc.ClientConn {
          connOnce.Do(func() {
              var err error
              conn, err = grpc.Dial(
                  "localhost:50051",
                  grpc.WithTransportCredentials(insecure.NewCredentials()),
                  grpc.WithDefaultCallOptions(
                      grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB
                      grpc.MaxCallSendMsgSize(10*1024*1024),
                  ),
              )
              if err != nil {
                  log.Fatalf("Failed to dial: %v", err)
              }
          })
          return conn
      }
      ```
      
      ### Keep-Alive Settings
      
      ```go
      // Server keep-alive
      s := grpc.NewServer(
          grpc.KeepaliveParams(keepalive.ServerParameters{
              Time:    10 * time.Second, // Ping every 10s if no activity
              Timeout: 3 * time.Second,  // Wait 3s for pong
          }),
          grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
              MinTime:             5 * time.Second, // Min time between pings
              PermitWithoutStream: true,            // Allow pings when no streams
          }),
      )
      
      // Client keep-alive
      conn, err := grpc.Dial(
          "localhost:50051",
          grpc.WithKeepaliveParams(keepalive.ClientParameters{
              Time:                10 * time.Second,
              Timeout:             3 * time.Second,
              PermitWithoutStream: true,
          }),
      )
      ```
      
      ### Compression
      
      ```go
      // Enable gzip compression
      conn, err := grpc.Dial(
          "localhost:50051",
          grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")),
      )
      
      // Per-call compression
      user, err := client.GetUser(
          ctx,
          req,
          grpc.UseCompressor("gzip"),
      )
      ```
      
      ## TLS/SSL Security
      
      ### Server TLS
      
      ```go
      import "google.golang.org/grpc/credentials"
      
      creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
      if err != nil {
          log.Fatalf("Failed to load TLS: %v", err)
      }
      
      s := grpc.NewServer(grpc.Creds(creds))
      ```
      
      ### Client TLS
      
      ```go
      creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
      if err != nil {
          log.Fatalf("Failed to load TLS: %v", err)
      }
      
      conn, err := grpc.Dial(
          "localhost:50051",
          grpc.WithTransportCredentials(creds),
      )
      ```
      
      ### Mutual TLS (mTLS)
      
      ```go
      // Server
      cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
      certPool := x509.NewCertPool()
      ca, _ := ioutil.ReadFile("ca.crt")
      certPool.AppendCertsFromPEM(ca)
      
      creds := credentials.NewTLS(&tls.Config{
          ClientAuth:   tls.RequireAndVerifyClientCert,
          Certificates: []tls.Certificate{cert},
          ClientCAs:    certPool,
      })
      
      s := grpc.NewServer(grpc.Creds(creds))
      ```
      
      ## Health Checking
      
      ```protobuf
      syntax = "proto3";
      
      package grpc.health.v1;
      
      service Health {
        rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
        rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
      }
      
      message HealthCheckRequest {
        string service = 1;
      }
      
      message HealthCheckResponse {
        enum ServingStatus {
          UNKNOWN = 0;
          SERVING = 1;
          NOT_SERVING = 2;
          SERVICE_UNKNOWN = 3;
        }
        ServingStatus status = 1;
      }
      ```
      
      **Implementation**:
      ```go
      import "google.golang.org/grpc/health/grpc_health_v1"
      
      healthServer := health.NewServer()
      healthServer.SetServingStatus("users.v1.UserService", grpc_health_v1.HealthCheckResponse_SERVING)
      
      grpc_health_v1.RegisterHealthServer(s, healthServer)
      ```
      
      ## Testing
      
      ### Unit Testing
      
      ```go
      import (
          "testing"
      
          "google.golang.org/grpc/codes"
          "google.golang.org/grpc/status"
      )
      
      func TestGetUser(t *testing.T) {
          mockDB := &MockDatabase{
              users: map[string]*User{
                  "123": {ID: "123", Email: "test@example.com"},
              },
          }
      
          srv := &server{db: mockDB}
      
          user, err := srv.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
      
          if err != nil {
              t.Fatalf("unexpected error: %v", err)
          }
      
          if user.Email != "test@example.com" {
              t.Errorf("expected test@example.com, got %s", user.Email)
          }
      
          // Test not found
          _, err = srv.GetUser(context.Background(), &pb.GetUserRequest{Id: "999"})
          if status.Code(err) != codes.NotFound {
              t.Errorf("expected NotFound, got %v", status.Code(err))
          }
      }
      ```
      
      ### Integration Testing
      
      ```go
      func TestUserService(t *testing.T) {
          // Start test server
          lis := bufconn.Listen(1024 * 1024)
          s := grpc.NewServer()
          pb.RegisterUserServiceServer(s, &server{db: testDB})
      
          go func() {
              if err := s.Serve(lis); err != nil {
                  log.Fatalf("Server exited with error: %v", err)
              }
          }()
          defer s.Stop()
      
          // Create test client
          conn, err := grpc.DialContext(
              context.Background(),
              "bufnet",
              grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
                  return lis.Dial()
              }),
              grpc.WithInsecure(),
          )
          if err != nil {
              t.Fatalf("Failed to dial: %v", err)
          }
          defer conn.Close()
      
          client := pb.NewUserServiceClient(conn)
      
          // Test GetUser
          user, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
          if err != nil {
              t.Fatalf("GetUser failed: %v", err)
          }
      
          if user.Id != "123" {
              t.Errorf("expected ID 123, got %s", user.Id)
          }
      }
      ```
      
      ## Best Practices Summary
      
      ✅ **Use proto3**: Modern syntax, better performance
      ✅ **Version services**: Use package versioning (`users.v1`)
      ✅ **Reserve field numbers**: Protect against breaking changes
      ✅ **Use well-known types**: Timestamp, Duration, Empty, Any
      ✅ **Implement health checks**: For load balancers, Kubernetes
      ✅ **Enable TLS**: Encrypt traffic in production
      ✅ **Add interceptors**: Logging, auth, metrics
      ✅ **Use keep-alive**: Maintain long-lived connections
      ✅ **Stream large datasets**: Avoid large unary responses
      ✅ **Handle errors properly**: Use correct status codes with details
      
      ❌ **Don't reuse field numbers**: Reserved fields protect from bugs
      ❌ **Don't use HTTP/JSON for gRPC**: Use binary Protobuf
      ❌ **Don't ignore deadlines**: Always set request timeouts
      ❌ **Don't skip error details**: Provide actionable error info
      ❌ **Don't run without TLS**: Production must use encryption
      ❌ **Don't forget connection pooling**: Reuse connections
      
      ## Additional Resources
      
      - [gRPC Official Documentation](https://grpc.io/docs/)
      - [Protocol Buffers Guide](https://developers.google.com/protocol-buffers/docs/proto3)
      - [gRPC Go Examples](https://github.com/grpc/grpc-go/tree/master/examples)
      - [gRPC Best Practices](https://grpc.io/docs/guides/performance/)
      - [gRPC Error Handling](https://grpc.io/docs/guides/error/)
      - [Awesome gRPC](https://github.com/grpc-ecosystem/awesome-grpc)
      
    • rest-patterns.md 18.3 KB
      # REST API Patterns - Deep Dive
      
      Comprehensive REST API design patterns covering advanced resource modeling, filtering, field selection, HATEOAS, and optimization techniques.
      
      ## Resource Modeling
      
      ### Single vs Collection Resources
      
      **Collection Resources** (plural nouns):
      ```
      GET    /users              → List all users
      POST   /users              → Create new user
      ```
      
      **Single Resources** (with ID):
      ```
      GET    /users/123          → Get specific user
      PUT    /users/123          → Replace user
      PATCH  /users/123          → Update user fields
      DELETE /users/123          → Delete user
      ```
      
      ### Sub-Resources (Nested Relationships)
      
      ✅ **Good: Clear hierarchy, logical nesting**
      ```
      GET    /users/123/orders          → User's orders
      POST   /users/123/orders          → Create order for user
      GET    /users/123/orders/456      → Specific order for user
      DELETE /users/123/orders/456      → Cancel user's order
      ```
      
      ❌ **Bad: Excessive nesting**
      ```
      GET /organizations/1/departments/2/teams/3/members/4/tasks/5
      ```
      ✅ **Better: Shallow hierarchy, use query params**
      ```
      GET /tasks/5
      GET /tasks?member_id=4&team_id=3
      ```
      
      ### Non-CRUD Actions
      
      When operations don't map to CRUD:
      
      **Option 1: Treat as sub-resource**
      ```
      POST /orders/123/cancel           → Cancel order
      POST /users/123/activate          → Activate user
      POST /invoices/456/send           → Send invoice
      ```
      
      **Option 2: Use controller-style endpoints** (less RESTful but pragmatic)
      ```
      POST /search                      → Complex search
      POST /bulk-operations             → Batch operations
      ```
      
      **Option 3: Use status field updates**
      ```
      PATCH /orders/123
      { "status": "cancelled", "reason": "Customer request" }
      ```
      
      ## HTTP Methods Deep Dive
      
      ### GET (Safe, Idempotent, Cacheable)
      
      **Characteristics**:
      - No side effects (safe)
      - Multiple identical requests = same result (idempotent)
      - Should be cached
      - No request body
      
      ```http
      GET /users?status=active&role=admin HTTP/1.1
      Host: api.example.com
      Accept: application/json
      Authorization: Bearer token123
      ```
      
      **Response**:
      ```http
      HTTP/1.1 200 OK
      Content-Type: application/json
      Cache-Control: public, max-age=300
      ETag: "abc123"
      
      {
        "data": [
          { "id": "1", "name": "Alice", "role": "admin" }
        ],
        "meta": {
          "total": 1,
          "page": 1,
          "per_page": 20
        }
      }
      ```
      
      ### POST (Not Safe, Not Idempotent)
      
      **Use for**:
      - Creating resources
      - Operations with side effects
      - Searches with complex body
      - Bulk operations
      
      ```http
      POST /users HTTP/1.1
      Host: api.example.com
      Content-Type: application/json
      
      {
        "email": "alice@example.com",
        "name": "Alice",
        "role": "admin"
      }
      ```
      
      **Response**:
      ```http
      HTTP/1.1 201 Created
      Location: /users/123
      Content-Type: application/json
      
      {
        "id": "123",
        "email": "alice@example.com",
        "name": "Alice",
        "role": "admin",
        "created_at": "2025-01-01T00:00:00Z"
      }
      ```
      
      ### PUT (Idempotent, Full Replace)
      
      **Characteristics**:
      - Replaces entire resource
      - Must include all fields
      - Idempotent (same request multiple times = same result)
      - Creates if doesn't exist (optional)
      
      ```http
      PUT /users/123 HTTP/1.1
      Content-Type: application/json
      If-Match: "abc123"
      
      {
        "email": "alice@example.com",
        "name": "Alice Smith",
        "role": "admin",
        "department": "engineering"
      }
      ```
      
      ### PATCH (Idempotent, Partial Update)
      
      **Use for**: Updating specific fields without replacing entire resource
      
      ```http
      PATCH /users/123 HTTP/1.1
      Content-Type: application/json
      
      {
        "name": "Alice Smith"
      }
      ```
      
      **JSON Patch (RFC 6902)** - more expressive:
      ```http
      PATCH /users/123 HTTP/1.1
      Content-Type: application/json-patch+json
      
      [
        { "op": "replace", "path": "/name", "value": "Alice Smith" },
        { "op": "add", "path": "/tags/-", "value": "premium" },
        { "op": "remove", "path": "/temporary_flag" }
      ]
      ```
      
      ### DELETE (Idempotent)
      
      ```http
      DELETE /users/123 HTTP/1.1
      ```
      
      **Response options**:
      ```http
      # Option 1: No content
      HTTP/1.1 204 No Content
      
      # Option 2: Return deleted resource
      HTTP/1.1 200 OK
      Content-Type: application/json
      { "id": "123", "deleted_at": "2025-01-01T00:00:00Z" }
      
      # Option 3: Already deleted (still success)
      HTTP/1.1 204 No Content
      ```
      
      ### HEAD (Metadata Only)
      
      Same as GET but no response body:
      ```http
      HEAD /users/123 HTTP/1.1
      
      HTTP/1.1 200 OK
      Content-Length: 256
      Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
      ETag: "abc123"
      ```
      
      ### OPTIONS (CORS, API Discovery)
      
      ```http
      OPTIONS /users HTTP/1.1
      
      HTTP/1.1 204 No Content
      Allow: GET, POST, HEAD, OPTIONS
      Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS
      Access-Control-Allow-Headers: Authorization, Content-Type
      ```
      
      ## Query Parameters
      
      ### Filtering
      
      ```
      # Single filter
      GET /users?status=active
      
      # Multiple filters (AND logic)
      GET /users?status=active&role=admin&department=engineering
      
      # Range filters
      GET /users?created_after=2025-01-01&created_before=2025-12-31
      
      # IN filters
      GET /users?id=1,2,3,4
      GET /users?status=active,pending
      
      # Pattern matching (use carefully, can be expensive)
      GET /users?name_like=alice
      GET /users?email_ends_with=@example.com
      ```
      
      **Advanced filtering with query operators**:
      ```
      GET /users?age[gte]=18&age[lte]=65
      GET /products?price[gt]=100&price[lt]=1000
      GET /posts?published[eq]=true
      ```
      
      ### Sorting
      
      ```
      # Single field
      GET /users?sort=created_at
      
      # Descending
      GET /users?sort=-created_at
      
      # Multiple fields
      GET /users?sort=last_name,first_name
      GET /users?sort=-created_at,name
      ```
      
      **Alternative formats**:
      ```
      GET /users?order_by=created_at&order=desc
      GET /users?sort[created_at]=desc&sort[name]=asc
      ```
      
      ### Field Selection (Sparse Fieldsets)
      
      Reduce payload size by requesting only needed fields:
      
      ```
      # Select specific fields
      GET /users?fields=id,name,email
      
      # Exclude fields
      GET /users?fields_exclude=internal_notes,password_hash
      
      # Nested field selection
      GET /users?fields=id,name,profile(avatar,bio)
      ```
      
      **Response**:
      ```json
      {
        "data": [
          {
            "id": "123",
            "name": "Alice",
            "email": "alice@example.com"
          }
        ]
      }
      ```
      
      ### Expansion (Include Related Resources)
      
      Avoid N+1 queries by including related data:
      
      ```
      # Basic expansion
      GET /orders/123?expand=customer
      
      # Multiple expansions
      GET /orders/123?expand=customer,items
      
      # Nested expansion
      GET /orders/123?expand=customer,items.product
      
      # Selective nested fields
      GET /orders/123?expand=customer(name,email),items(quantity,price)
      ```
      
      **Response**:
      ```json
      {
        "id": "123",
        "total": 1500,
        "customer": {
          "id": "456",
          "name": "Alice",
          "email": "alice@example.com"
        },
        "items": [
          {
            "id": "789",
            "quantity": 2,
            "price": 750,
            "product": {
              "id": "101",
              "name": "Widget",
              "sku": "WDG-001"
            }
          }
        ]
      }
      ```
      
      ## Pagination Patterns
      
      ### Offset Pagination
      
      **Simple and familiar**:
      ```
      GET /users?limit=20&offset=0   # Page 1
      GET /users?limit=20&offset=20  # Page 2
      GET /users?limit=20&offset=40  # Page 3
      ```
      
      **Response format**:
      ```json
      {
        "data": [...],
        "meta": {
          "total": 1543,
          "limit": 20,
          "offset": 40,
          "page": 3,
          "total_pages": 78
        },
        "links": {
          "first": "/users?limit=20&offset=0",
          "prev": "/users?limit=20&offset=20",
          "next": "/users?limit=20&offset=60",
          "last": "/users?limit=20&offset=1540"
        }
      }
      ```
      
      **Pros**:
      - Easy to implement
      - Supports random access (jump to page 10)
      - Familiar to users
      
      **Cons**:
      - Performance degrades with high offsets (database skips rows)
      - Inconsistent results if data changes (items shift between pages)
      - Not suitable for real-time feeds
      
      ### Cursor Pagination
      
      **Efficient and stable**:
      ```
      GET /users?limit=20                        # First page
      GET /users?limit=20&cursor=eyJpZCI6MjB9   # Next page
      ```
      
      **Response format**:
      ```json
      {
        "data": [...],
        "meta": {
          "limit": 20,
          "has_more": true
        },
        "cursors": {
          "before": "eyJpZCI6MX0",
          "after": "eyJpZCI6MjB9"
        },
        "links": {
          "next": "/users?limit=20&cursor=eyJpZCI6MjB9",
          "prev": "/users?limit=20&cursor=eyJpZCI6MX0&direction=prev"
        }
      }
      ```
      
      **Cursor encoding** (base64 JSON):
      ```typescript
      // Encode cursor
      const cursor = Buffer.from(JSON.stringify({ id: 20 })).toString('base64url');
      
      // Decode cursor
      const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
      ```
      
      **Pros**:
      - Consistent results even if data changes
      - Efficient for large datasets
      - No offset performance penalty
      
      **Cons**:
      - No random access (can't jump to page 10)
      - More complex to implement
      - Cursor reveals internal structure (encrypt if sensitive)
      
      ### Keyset Pagination
      
      **Database-optimized**:
      ```
      GET /users?limit=20&after_id=123&order=id
      ```
      
      **SQL implementation**:
      ```sql
      -- First page
      SELECT * FROM users ORDER BY id ASC LIMIT 20;
      
      -- Next page (after_id from last result)
      SELECT * FROM users WHERE id > 123 ORDER BY id ASC LIMIT 20;
      ```
      
      **Pros**:
      - Most performant (uses database index)
      - Simple implementation
      - Stable results
      
      **Cons**:
      - Requires ordered, unique field
      - No backward pagination (easily)
      - Complex with multi-field sorting
      
      ### Page Number Pagination
      
      **User-friendly**:
      ```
      GET /users?page=1&per_page=20
      GET /users?page=2&per_page=20
      ```
      
      **Response**:
      ```json
      {
        "data": [...],
        "meta": {
          "current_page": 2,
          "per_page": 20,
          "total": 1543,
          "total_pages": 78,
          "from": 21,
          "to": 40
        },
        "links": {
          "first": "/users?page=1&per_page=20",
          "prev": "/users?page=1&per_page=20",
          "next": "/users?page=3&per_page=20",
          "last": "/users?page=78&per_page=20"
        }
      }
      ```
      
      Same pros/cons as offset pagination (it's offset in disguise: `offset = (page - 1) * per_page`).
      
      ## HATEOAS (Hypermedia)
      
      **Hypermedia As The Engine Of Application State**: Include links to related actions and resources.
      
      ### Basic HATEOAS
      
      ```json
      {
        "id": "123",
        "name": "Alice",
        "email": "alice@example.com",
        "links": {
          "self": "/users/123",
          "orders": "/users/123/orders",
          "edit": "/users/123",
          "delete": "/users/123"
        }
      }
      ```
      
      ### HAL (Hypertext Application Language)
      
      ```json
      {
        "_links": {
          "self": { "href": "/orders/123" },
          "customer": { "href": "/customers/456" },
          "payment": { "href": "/payments/789" }
        },
        "id": "123",
        "total": 1500,
        "status": "shipped",
        "_embedded": {
          "customer": {
            "_links": { "self": { "href": "/customers/456" } },
            "id": "456",
            "name": "Alice"
          }
        }
      }
      ```
      
      ### JSON:API
      
      ```json
      {
        "data": {
          "type": "orders",
          "id": "123",
          "attributes": {
            "total": 1500,
            "status": "shipped"
          },
          "relationships": {
            "customer": {
              "links": {
                "self": "/orders/123/relationships/customer",
                "related": "/orders/123/customer"
              },
              "data": { "type": "customers", "id": "456" }
            }
          },
          "links": {
            "self": "/orders/123"
          }
        },
        "included": [
          {
            "type": "customers",
            "id": "456",
            "attributes": {
              "name": "Alice",
              "email": "alice@example.com"
            }
          }
        ]
      }
      ```
      
      ## Batch Operations
      
      ### Batch Create
      
      ```http
      POST /users/batch HTTP/1.1
      Content-Type: application/json
      
      {
        "items": [
          { "email": "alice@example.com", "name": "Alice" },
          { "email": "bob@example.com", "name": "Bob" }
        ]
      }
      ```
      
      **Response**:
      ```json
      {
        "results": [
          {
            "status": 201,
            "id": "123",
            "email": "alice@example.com"
          },
          {
            "status": 201,
            "id": "124",
            "email": "bob@example.com"
          }
        ],
        "summary": {
          "total": 2,
          "succeeded": 2,
          "failed": 0
        }
      }
      ```
      
      ### Batch Update
      
      ```http
      PATCH /users/batch HTTP/1.1
      Content-Type: application/json
      
      {
        "updates": [
          { "id": "123", "status": "active" },
          { "id": "124", "status": "inactive" }
        ]
      }
      ```
      
      ### Batch Get
      
      ```http
      GET /users?id=123,124,125 HTTP/1.1
      
      # Or POST for large lists
      POST /users/batch/get HTTP/1.1
      { "ids": ["123", "124", "125", ...] }
      ```
      
      ## Async Operations
      
      ### Long-Running Operations
      
      **Pattern**: Return 202 Accepted with status URL:
      
      ```http
      POST /reports/generate HTTP/1.1
      { "type": "annual", "year": 2024 }
      
      HTTP/1.1 202 Accepted
      Location: /operations/op_abc123
      Content-Type: application/json
      
      {
        "operation_id": "op_abc123",
        "status": "pending",
        "status_url": "/operations/op_abc123",
        "estimated_completion": "2025-01-01T00:05:00Z"
      }
      ```
      
      **Status endpoint**:
      ```http
      GET /operations/op_abc123
      
      # While processing
      HTTP/1.1 200 OK
      {
        "id": "op_abc123",
        "status": "processing",
        "progress": 45,
        "message": "Generating report..."
      }
      
      # When complete
      HTTP/1.1 303 See Other
      Location: /reports/rep_xyz789
      {
        "id": "op_abc123",
        "status": "completed",
        "result_url": "/reports/rep_xyz789"
      }
      ```
      
      ## Compression
      
      **Request compression** (rare, large request bodies):
      ```http
      POST /data/import HTTP/1.1
      Content-Encoding: gzip
      Content-Type: application/json
      ```
      
      **Response compression** (common):
      ```http
      GET /users HTTP/1.1
      Accept-Encoding: gzip, deflate, br
      
      HTTP/1.1 200 OK
      Content-Encoding: gzip
      Content-Type: application/json
      ```
      
      Enable compression for responses >1KB. Use Brotli (br) for best compression.
      
      ## Content Negotiation
      
      ```http
      # Request JSON
      GET /users/123
      Accept: application/json
      
      # Request XML
      GET /users/123
      Accept: application/xml
      
      # Request specific version
      GET /users/123
      Accept: application/vnd.myapi.v2+json
      
      # Multiple acceptable types (quality values)
      GET /users/123
      Accept: application/json; q=1.0, application/xml; q=0.8
      ```
      
      ## Conditional Requests
      
      ### ETags (Strong Validation)
      
      ```http
      # Get with ETag
      GET /users/123
      Response: ETag: "abc123"
      
      # Update only if unchanged
      PUT /users/123
      If-Match: "abc123"
      { "name": "Alice Smith" }
      
      # Success if ETag matches
      HTTP/1.1 200 OK
      
      # Failure if ETag changed (concurrent update)
      HTTP/1.1 412 Precondition Failed
      {
        "error": "Resource was modified by another request",
        "current_etag": "def456"
      }
      ```
      
      ### Last-Modified (Weak Validation)
      
      ```http
      GET /users/123
      Response: Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
      
      PUT /users/123
      If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT
      ```
      
      ## Performance Optimization
      
      ### HTTP/2 and HTTP/3
      
      - **Multiplexing**: Multiple requests over single connection
      - **Server Push**: Proactively send resources (use carefully)
      - **Header compression**: HPACK reduces overhead
      
      Enable HTTP/2 in production:
      ```nginx
      listen 443 ssl http2;
      ```
      
      ### Connection Pooling
      
      Reuse TCP connections:
      ```typescript
      import http from 'http';
      
      const agent = new http.Agent({
        keepAlive: true,
        maxSockets: 50,
        maxFreeSockets: 10,
        timeout: 60000
      });
      
      fetch('https://api.example.com/users', { agent });
      ```
      
      ### Response Streaming
      
      Stream large responses:
      ```typescript
      app.get('/export', (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.write('[');
      
        const stream = db.users.stream();
        let first = true;
      
        stream.on('data', (user) => {
          if (!first) res.write(',');
          res.write(JSON.stringify(user));
          first = false;
        });
      
        stream.on('end', () => {
          res.write(']');
          res.end();
        });
      });
      ```
      
      ## Security Headers
      
      ```http
      HTTP/1.1 200 OK
      Strict-Transport-Security: max-age=31536000; includeSubDomains
      X-Content-Type-Options: nosniff
      X-Frame-Options: DENY
      Content-Security-Policy: default-src 'self'
      X-XSS-Protection: 1; mode=block
      ```
      
      ## CORS (Cross-Origin Resource Sharing)
      
      **Preflight request** (OPTIONS):
      ```http
      OPTIONS /users HTTP/1.1
      Origin: https://example.com
      Access-Control-Request-Method: POST
      Access-Control-Request-Headers: Content-Type, Authorization
      
      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: https://example.com
      Access-Control-Allow-Methods: GET, POST, PUT, DELETE
      Access-Control-Allow-Headers: Content-Type, Authorization
      Access-Control-Max-Age: 86400
      ```
      
      **Actual request**:
      ```http
      POST /users HTTP/1.1
      Origin: https://example.com
      
      HTTP/1.1 201 Created
      Access-Control-Allow-Origin: https://example.com
      Access-Control-Expose-Headers: Location, X-Request-Id
      ```
      
      ## REST API Testing
      
      ### Integration Tests
      
      ```typescript
      import request from 'supertest';
      import { app } from './app';
      
      describe('User API', () => {
        it('creates a user', async () => {
          const response = await request(app)
            .post('/users')
            .send({ email: 'test@example.com', name: 'Test' })
            .expect(201)
            .expect('Content-Type', /json/);
      
          expect(response.body).toHaveProperty('id');
          expect(response.headers.location).toBe(`/users/${response.body.id}`);
        });
      
        it('returns 404 for non-existent user', async () => {
          await request(app)
            .get('/users/999')
            .expect(404);
        });
      
        it('validates email format', async () => {
          const response = await request(app)
            .post('/users')
            .send({ email: 'invalid', name: 'Test' })
            .expect(400);
      
          expect(response.body.error.details).toContainEqual(
            expect.objectContaining({ field: 'email' })
          );
        });
      });
      ```
      
      ### API Schema Validation
      
      ```typescript
      import Ajv from 'ajv';
      import { openApiSchema } from './openapi.json';
      
      const ajv = new Ajv();
      const validate = ajv.compile(openApiSchema.components.schemas.User);
      
      it('response matches OpenAPI schema', async () => {
        const response = await request(app).get('/users/123');
        const valid = validate(response.body);
        expect(valid).toBe(true);
      });
      ```
      
      ## Best Practices Summary
      
      ✅ **Use plural nouns for collections**: `/users` not `/user`
      ✅ **Use HTTP methods correctly**: GET (read), POST (create), PUT/PATCH (update), DELETE (delete)
      ✅ **Return appropriate status codes**: 200, 201, 400, 404, 500, etc.
      ✅ **Version your API**: `/v1/users` or header-based
      ✅ **Support pagination**: Offset, cursor, or keyset
      ✅ **Include HATEOAS links**: Help clients discover actions
      ✅ **Use ETags for caching**: Conditional requests (If-Match, If-None-Match)
      ✅ **Compress responses**: gzip, Brotli for >1KB
      ✅ **Implement rate limiting**: Protect against abuse
      ✅ **Document with OpenAPI**: Interactive, machine-readable docs
      ✅ **Test thoroughly**: Unit, integration, contract, load tests
      
      ❌ **Avoid verbs in URLs**: `/getUser` should be `GET /users/{id}`
      ❌ **Don't ignore HTTP semantics**: Use correct methods and status codes
      ❌ **Don't over-nest resources**: Keep hierarchy shallow (2-3 levels max)
      ❌ **Don't return entire objects**: Support field selection for efficiency
      ❌ **Don't break existing versions**: Version breaking changes
      ❌ **Don't expose internal structure**: Abstract implementation details
      ❌ **Don't skip error details**: Provide actionable error messages
      
      ## Additional Resources
      
      - [RFC 7231: HTTP/1.1 Semantics](https://tools.ietf.org/html/rfc7231)
      - [RFC 6902: JSON Patch](https://tools.ietf.org/html/rfc6902)
      - [RFC 5988: Web Linking](https://tools.ietf.org/html/rfc5988)
      - [JSON:API Specification](https://jsonapi.org/)
      - [HAL Specification](https://stateless.group/hal_specification.html)
      - [OpenAPI 3.1.0 Specification](https://spec.openapis.org/oas/v3.1.0)
      
    • versioning-strategies.md 17.5 KB
      # API Versioning Strategies - Deep Dive
      
      Comprehensive guide to API versioning patterns, migration strategies, and managing breaking changes across REST, GraphQL, and gRPC.
      
      ## Why Version APIs?
      
      **Breaking changes**:
      - Removing fields or endpoints
      - Renaming fields or endpoints
      - Changing field types
      - Changing required/optional status
      - Changing response structure
      - Changing authentication methods
      - Changing error formats
      
      **Non-breaking changes**:
      - Adding optional fields
      - Adding new endpoints
      - Adding new enum values (append-only)
      - Expanding validation (less strict)
      - Adding optional query parameters
      - Adding new error codes (with graceful handling)
      
      ## URI Versioning
      
      ### Pattern
      
      ```
      GET /v1/users
      GET /v2/users
      GET /v3/users
      ```
      
      ### Pros
      
      ✅ **Simple and explicit**: Version is immediately visible
      ✅ **Easy to route**: Straightforward load balancer/proxy rules
      ✅ **Browser-friendly**: Can test in browser address bar
      ✅ **Industry standard**: Used by Stripe, Twitter, GitHub
      ✅ **Clear deprecation**: Sunset entire version
      
      ### Cons
      
      ❌ **URL pollution**: Versions in every URL
      ❌ **Resource duplication**: `/v1/users/123` vs `/v2/users/123` same resource
      ❌ **Cache complications**: Separate cache keys per version
      ❌ **Not RESTful**: Violates resource identifier principle
      
      ### Implementation
      
      ```typescript
      // Express routing
      app.use('/v1', require('./routes/v1'));
      app.use('/v2', require('./routes/v2'));
      app.use('/v3', require('./routes/v3'));
      
      // Redirect /users to latest version
      app.get('/users', (req, res) => {
        res.redirect(301, '/v3/users');
      });
      ```
      
      **Nginx routing**:
      ```nginx
      location /v1/ {
          proxy_pass http://api-v1:3000/;
      }
      
      location /v2/ {
          proxy_pass http://api-v2:3000/;
      }
      
      location /v3/ {
          proxy_pass http://api-v3:3000/;
      }
      ```
      
      ### Versioning Scheme
      
      **Major version only** (recommended):
      ```
      /v1/users
      /v2/users
      ```
      
      **Semantic versioning** (overkill for APIs):
      ```
      /v1.2.3/users  # Too granular
      ```
      
      **Date-based versioning**:
      ```
      /2024-01-15/users  # Stripe-style
      ```
      
      ## Header Versioning
      
      ### Accept Header
      
      ```http
      GET /users
      Accept: application/vnd.myapi.v2+json
      ```
      
      **Vendor MIME types**:
      ```
      application/vnd.myapi.v1+json
      application/vnd.myapi.v2+json
      application/vnd.github.v3+json
      ```
      
      ### Custom Header
      
      ```http
      GET /users
      API-Version: 2
      ```
      
      Or:
      ```http
      GET /users
      X-API-Version: 2024-01-15
      ```
      
      ### Pros
      
      ✅ **Clean URLs**: Version separate from resource path
      ✅ **RESTful**: Resource identifiers unchanged
      ✅ **Flexible**: Can version per-resource
      ✅ **HTTP standard**: Uses existing headers
      
      ### Cons
      
      ❌ **Less visible**: Not in URL, harder to debug
      ❌ **Harder to test**: Can't test in browser easily
      ❌ **Documentation complexity**: Requires explanation
      ❌ **Client complexity**: More header management
      
      ### Implementation
      
      ```typescript
      // Express middleware
      app.use((req, res, next) => {
        const acceptHeader = req.get('Accept') || '';
        const versionMatch = acceptHeader.match(/vnd\.myapi\.v(\d+)\+json/);
      
        req.apiVersion = versionMatch ? parseInt(versionMatch[1]) : 1;
        next();
      });
      
      // Route based on version
      app.get('/users', (req, res) => {
        if (req.apiVersion === 1) {
          return v1.getUsers(req, res);
        } else if (req.apiVersion === 2) {
          return v2.getUsers(req, res);
        } else {
          return res.status(400).json({ error: 'Unsupported API version' });
        }
      });
      ```
      
      **Default version fallback**:
      ```typescript
      app.use((req, res, next) => {
        const apiVersion = req.get('API-Version');
        req.apiVersion = apiVersion ? parseInt(apiVersion) : 2; // Default to v2
        next();
      });
      ```
      
      ## Content Negotiation
      
      ### Resource-Level Versioning
      
      ```http
      GET /users
      Accept: application/vnd.myapi.user.v2+json
      ```
      
      Different resources can have different versions:
      ```http
      Accept: application/vnd.myapi.user.v2+json
      Accept: application/vnd.myapi.order.v3+json
      ```
      
      ### Pros
      
      ✅ **Granular**: Version per resource type
      ✅ **Evolutionary**: Resources evolve independently
      ✅ **Backward compatible**: Mix old and new versions
      
      ### Cons
      
      ❌ **Complexity**: More versions to manage
      ❌ **Testing burden**: Combinatorial explosion
      ❌ **Client confusion**: Which resource versions are compatible?
      
      ## Query Parameter Versioning
      
      ```
      GET /users?version=2
      GET /users?api_version=2
      GET /users?v=2
      ```
      
      ### Pros
      
      ✅ **Simple**: Easy to implement
      ✅ **Visible**: In URL for debugging
      ✅ **Optional**: Can default to latest
      
      ### Cons
      
      ❌ **Pollutes query params**: Mixes versioning with filtering
      ❌ **Not RESTful**: Query params should be for filtering
      ❌ **Cache issues**: May need to handle in caching layer
      ❌ **Less professional**: Rarely used in production APIs
      
      ### Implementation
      
      ```typescript
      app.get('/users', (req, res) => {
        const version = parseInt(req.query.version) || 2;
      
        if (version === 1) {
          return v1.getUsers(req, res);
        } else if (version === 2) {
          return v2.getUsers(req, res);
        } else {
          return res.status(400).json({ error: 'Invalid API version' });
        }
      });
      ```
      
      ## Version Negotiation
      
      ### Default Version
      
      **Option 1: Latest version by default**
      ```typescript
      const version = req.apiVersion || LATEST_VERSION;
      ```
      
      **Option 2: Require explicit version**
      ```typescript
      if (!req.apiVersion) {
        return res.status(400).json({
          error: 'API version required',
          documentation: 'https://api.example.com/docs/versioning'
        });
      }
      ```
      
      **Option 3: Pin to first version on API key creation**
      ```typescript
      // API key includes default version
      const apiKey = await createAPIKey({ userId, defaultVersion: 2 });
      
      // Use key's default version if not specified
      const version = req.apiVersion || apiKey.defaultVersion;
      ```
      
      ### Version Discovery
      
      **Include supported versions in response**:
      ```http
      GET /
      
      HTTP/1.1 200 OK
      API-Version: 2
      API-Versions-Supported: 1, 2, 3
      API-Versions-Deprecated: 1
      
      {
        "current_version": 2,
        "supported_versions": [1, 2, 3],
        "deprecated_versions": [1],
        "latest_version": 3,
        "documentation": "https://api.example.com/docs"
      }
      ```
      
      ## Deprecation Strategy
      
      ### Deprecation Timeline
      
      **Phase 1: Announce (3-6 months ahead)**
      ```json
      {
        "version": "1",
        "status": "active",
        "sunset_date": "2025-12-31",
        "replacement_version": "2",
        "migration_guide": "https://api.example.com/docs/v1-to-v2"
      }
      ```
      
      **Phase 2: Warn (headers + docs)**
      ```http
      HTTP/1.1 200 OK
      Deprecation: true
      Sunset: Sat, 31 Dec 2025 23:59:59 GMT
      Link: <https://api.example.com/docs/v1-to-v2>; rel="deprecation"
      
      {
        "data": [...],
        "meta": {
          "deprecation_warning": "API v1 will be sunset on 2025-12-31. Migrate to v2."
        }
      }
      ```
      
      **Phase 3: Limit (rate limiting)**
      ```
      v1: 100 req/min → 50 req/min → 10 req/min
      v2: 1000 req/min (normal limits)
      ```
      
      **Phase 4: Sunset (remove)**
      ```http
      GET /v1/users
      
      HTTP/1.1 410 Gone
      {
        "error": "API v1 has been sunset",
        "sunset_date": "2025-12-31",
        "replacement": "https://api.example.com/v2/users",
        "migration_guide": "https://api.example.com/docs/v1-to-v2"
      }
      ```
      
      ### Deprecation Headers
      
      ```http
      # Deprecation (RFC draft)
      Deprecation: true
      Deprecation: @1640995200  # Unix timestamp
      
      # Sunset (RFC 8594)
      Sunset: Sat, 31 Dec 2025 23:59:59 GMT
      
      # Link to migration guide
      Link: <https://api.example.com/docs/v1-to-v2>; rel="deprecation"
      Link: <https://api.example.com/v2/users>; rel="alternate"
      ```
      
      ### Communication Channels
      
      1. **Email notifications**: Contact all API key owners
      2. **Changelog**: Document in API changelog
      3. **Developer portal**: Banner on docs site
      4. **Slack/Discord**: Developer community announcements
      5. **Blog post**: Major version changes
      6. **Status page**: Sunset timeline
      
      ## Migration Patterns
      
      ### Adapter Pattern (Shared Logic)
      
      ```typescript
      // Shared domain logic
      class UserService {
        async getUser(id: string) {
          return db.users.findUnique({ where: { id } });
        }
      }
      
      // V1 adapter
      class V1UserAdapter {
        constructor(private service: UserService) {}
      
        async getUser(id: string) {
          const user = await this.service.getUser(id);
          return {
            user_id: user.id,  // V1 format: snake_case
            email: user.email,
            full_name: user.name,
          };
        }
      }
      
      // V2 adapter
      class V2UserAdapter {
        constructor(private service: UserService) {}
      
        async getUser(id: string) {
          const user = await this.service.getUser(id);
          return {
            id: user.id,       // V2 format: camelCase
            email: user.email,
            name: user.name,
            createdAt: user.createdAt,
          };
        }
      }
      ```
      
      ### Feature Flags
      
      ```typescript
      // Gradual rollout of v2 features
      const featureFlags = {
        v2NewAuthFlow: { rollout: 0.1 },  // 10% of users
        v2NewErrorFormat: { rollout: 0.5 }, // 50% of users
      };
      
      app.get('/v2/users', async (req, res) => {
        const user = await getUser(req.params.id);
      
        // Use feature flag for new auth flow
        if (isEnabled('v2NewAuthFlow', user)) {
          await checkAuthV2(req);
        } else {
          await checkAuthV1(req);
        }
      
        res.json(user);
      });
      ```
      
      ### Dual-Write Pattern
      
      **Write to both versions during migration**:
      ```typescript
      async function updateUser(id: string, data: any) {
        // Write to v1 format (old database schema)
        await v1DB.users.update({
          where: { id },
          data: {
            full_name: data.name,
            email_address: data.email,
          },
        });
      
        // Write to v2 format (new database schema)
        await v2DB.users.update({
          where: { id },
          data: {
            name: data.name,
            email: data.email,
          },
        });
      }
      ```
      
      ### Transformation Layer
      
      ```typescript
      // Bidirectional transformations
      class UserTransformer {
        static toV1(v2User: V2User): V1User {
          return {
            user_id: v2User.id,
            email: v2User.email,
            full_name: v2User.name,
            created: v2User.createdAt.toISOString(),
          };
        }
      
        static toV2(v1User: V1User): V2User {
          return {
            id: v1User.user_id,
            email: v1User.email,
            name: v1User.full_name,
            createdAt: new Date(v1User.created),
          };
        }
      }
      
      // V1 endpoint (transform v2 data to v1 format)
      app.get('/v1/users/:id', async (req, res) => {
        const v2User = await getUserV2(req.params.id);
        const v1User = UserTransformer.toV1(v2User);
        res.json(v1User);
      });
      ```
      
      ## GraphQL Versioning
      
      ### Schema Evolution (Preferred)
      
      **Add new fields without breaking existing queries**:
      ```graphql
      type User {
        id: ID!
        email: String!
        name: String!
      
        # Add new field (non-breaking)
        fullName: String! @deprecated(reason: "Use name instead")
      
        # Add new optional field (non-breaking)
        phoneNumber: String
      
        # Add new field with default (non-breaking)
        role: UserRole! = USER
      }
      ```
      
      ### Field Deprecation
      
      ```graphql
      type User {
        id: ID!
        email: String!
      
        # Deprecated field
        name: String! @deprecated(reason: "Use firstName and lastName instead")
      
        # New fields
        firstName: String!
        lastName: String!
      }
      ```
      
      **Clients see deprecation in introspection**:
      ```json
      {
        "name": "name",
        "type": { "kind": "SCALAR", "name": "String" },
        "isDeprecated": true,
        "deprecationReason": "Use firstName and lastName instead"
      }
      ```
      
      ### Schema Versioning (Alternative)
      
      ```graphql
      # Schema v1
      type Query {
        user(id: ID!): UserV1
        users: [UserV1!]!
      }
      
      type UserV1 {
        id: ID!
        email: String!
        name: String!
      }
      
      # Schema v2
      type Query {
        userV2(id: ID!): UserV2
        usersV2: [UserV2!]!
      }
      
      type UserV2 {
        id: ID!
        email: String!
        firstName: String!
        lastName: String!
      }
      ```
      
      ### Version in Query
      
      ```graphql
      query GetUser($version: Int = 2) {
        user(id: "123", version: $version) {
          ... on UserV1 {
            id
            name
          }
          ... on UserV2 {
            id
            firstName
            lastName
          }
        }
      }
      ```
      
      ## gRPC Versioning
      
      ### Package Versioning (Recommended)
      
      ```protobuf
      // users/v1/user.proto
      syntax = "proto3";
      package users.v1;
      
      service UserService {
        rpc GetUser (GetUserRequest) returns (User) {}
      }
      
      // users/v2/user.proto
      syntax = "proto3";
      package users.v2;
      
      service UserService {
        rpc GetUser (GetUserRequest) returns (User) {}
      }
      ```
      
      **Server serves multiple versions**:
      ```go
      v1Server := &v1.UserServiceServer{}
      v2Server := &v2.UserServiceServer{}
      
      v1pb.RegisterUserServiceServer(s, v1Server)
      v2pb.RegisterUserServiceServer(s, v2Server)
      ```
      
      ### Field Evolution
      
      **Add fields (non-breaking)**:
      ```protobuf
      message User {
        string id = 1;
        string email = 2;
        string name = 3;
      
        // Add new optional field (non-breaking)
        string phone_number = 4;
      
        // Add new field with default (non-breaking)
        UserRole role = 5 [default = USER_ROLE_USER];
      }
      ```
      
      **Deprecate fields**:
      ```protobuf
      message User {
        string id = 1;
        string email = 2;
      
        // Deprecated field
        string name = 3 [deprecated = true];
      
        // New fields
        string first_name = 4;
        string last_name = 5;
      }
      ```
      
      ### Breaking Changes
      
      **Reserve field numbers**:
      ```protobuf
      message User {
        reserved 3;  // Field 3 removed
        reserved "old_field_name";
      
        string id = 1;
        string email = 2;
        // Field 3 cannot be reused
        string new_field = 4;
      }
      ```
      
      ## Version Compatibility Matrix
      
      | Change | REST URI | REST Header | GraphQL | gRPC |
      |--------|----------|-------------|---------|------|
      | Add optional field | ✅ Non-breaking | ✅ Non-breaking | ✅ Non-breaking | ✅ Non-breaking |
      | Add required field | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      | Remove field | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      | Rename field | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      | Change type | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      | Add endpoint | ✅ Non-breaking | ✅ Non-breaking | ✅ Non-breaking | ✅ Non-breaking |
      | Remove endpoint | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      | Add enum value | ⚠️ Depends | ⚠️ Depends | ⚠️ Depends | ✅ Non-breaking |
      | Remove enum value | ❌ Breaking | ❌ Breaking | ❌ Breaking | ❌ Breaking |
      
      ## Testing Multiple Versions
      
      ### Contract Testing
      
      ```typescript
      // Pact contract test for v1
      describe('User API v1', () => {
        it('gets a user', async () => {
          const provider = new Pact({
            consumer: 'Client',
            provider: 'UserAPI-v1',
          });
      
          await provider
            .given('user 123 exists')
            .uponReceiving('a request for user 123')
            .withRequest({
              method: 'GET',
              path: '/v1/users/123',
            })
            .willRespondWith({
              status: 200,
              body: {
                user_id: '123',
                email: 'test@example.com',
                full_name: 'Test User',
              },
            });
        });
      });
      
      // Pact contract test for v2
      describe('User API v2', () => {
        it('gets a user', async () => {
          await provider
            .given('user 123 exists')
            .uponReceiving('a request for user 123')
            .withRequest({
              method: 'GET',
              path: '/v2/users/123',
            })
            .willRespondWith({
              status: 200,
              body: {
                id: '123',
                email: 'test@example.com',
                name: 'Test User',
              },
            });
        });
      });
      ```
      
      ### Smoke Tests
      
      ```typescript
      // Test all supported versions
      const versions = ['v1', 'v2', 'v3'];
      
      for (const version of versions) {
        describe(`API ${version} smoke tests`, () => {
          it('gets users', async () => {
            const response = await fetch(`https://api.example.com/${version}/users`);
            expect(response.status).toBe(200);
          });
      
          it('creates user', async () => {
            const response = await fetch(`https://api.example.com/${version}/users`, {
              method: 'POST',
              body: JSON.stringify({ email: 'test@example.com' }),
            });
            expect(response.status).toBe(201);
          });
        });
      }
      ```
      
      ## Best Practices Summary
      
      ✅ **Version from day one**: Don't wait until you need to break things
      ✅ **Use URI versioning for REST**: Simple, explicit, industry standard
      ✅ **Use package versioning for gRPC**: `users.v1`, `users.v2`
      ✅ **Deprecate gracefully**: 3-6 month timeline with warnings
      ✅ **Document migration paths**: Provide clear upgrade guides
      ✅ **Support 2-3 versions max**: Don't accumulate technical debt
      ✅ **Test all versions**: Contract tests prevent regressions
      ✅ **Communicate early**: Email + docs + headers + blog posts
      ✅ **Use sunset headers**: `Deprecation`, `Sunset`, `Link`
      ✅ **Monitor version usage**: Track adoption before deprecating
      
      ❌ **Don't version minor changes**: Use feature flags instead
      ❌ **Don't break unexpectedly**: Always announce ahead of time
      ❌ **Don't support too many versions**: 2-3 active versions max
      ❌ **Don't remove versions suddenly**: Gradual deprecation timeline
      ❌ **Don't skip migration guides**: Document upgrade path clearly
      ❌ **Don't forget to sunset**: Old versions accumulate technical debt
      
      ## Decision Tree
      
      ```
      Should you create a new version?
      
      Breaking change?
      ├─ Yes → New version required
      │  ├─ Removed fields/endpoints → New major version
      │  ├─ Changed types → New major version
      │  ├─ Changed auth → New major version
      │  └─ Changed error format → New major version
      │
      └─ No → Add to current version
         ├─ Added optional fields → Non-breaking
         ├─ Added new endpoints → Non-breaking
         ├─ Relaxed validation → Non-breaking
         └─ Added enum values → Non-breaking (usually)
      
      Which versioning strategy?
      
      REST API?
      ├─ Public API → URI versioning (/v1/users)
      ├─ Internal API → Header versioning (API-Version: 2)
      └─ Microservices → Package versioning (users.v1)
      
      GraphQL API?
      ├─ Preferred → Schema evolution + @deprecated
      └─ Alternative → Versioned types (UserV1, UserV2)
      
      gRPC API?
      └─ Always → Package versioning (users.v1, users.v2)
      ```
      
      ## Additional Resources
      
      - [Semantic Versioning](https://semver.org/)
      - [RFC 8594: Sunset HTTP Header](https://tools.ietf.org/html/rfc8594)
      - [Stripe API Versioning](https://stripe.com/docs/api/versioning)
      - [GraphQL Best Practices](https://graphql.org/learn/best-practices/#versioning)
      - [gRPC Versioning Guide](https://grpc.io/docs/guides/api-versioning/)
      - [HTTP API Problem Details (RFC 7807)](https://tools.ietf.org/html/rfc7807)
      
  • SKILL.md 22 KB
    ---
    name: api-design-patterns
    description: Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
    license: Apache-2.0
    compatibility: claude-code
    metadata:
      version: 1.0.0
      category: universal
      related_skills: [graphql, typescript, nodejs-backend, django, fastapi, flask]
      token_budget:
        entry_point: 85
        full_content: 8500
      self_contained: true
    tags: [api, rest, graphql, grpc, architecture, web, design-patterns]
    progressive_disclosure:
      entry_point:
        summary: "Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices"
        when_to_use: "When designing, implementing, or documenting APIs."
        quick_start: "1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."
      references:
        - authentication.md
        - graphql-patterns.md
        - grpc-patterns.md
        - rest-patterns.md
        - versioning-strategies.md
    ---
    # API Design Patterns
    
    Design robust, scalable APIs using proven patterns for REST, GraphQL, and gRPC with proper versioning, authentication, and error handling.
    
    ## Quick Reference
    
    **API Style Selection**:
    - REST: Resource-based CRUD, simple clients, HTTP-native caching
    - GraphQL: Client-driven queries, complex data graphs, real-time subscriptions
    - gRPC: High-performance RPC, microservices, strong typing, streaming
    
    **Critical Patterns**:
    - Versioning: URI (`/v1/users`), header (`Accept: application/vnd.api+json;version=1`), content negotiation
    - Pagination: Offset (simple), cursor (stable), keyset (performant)
    - Auth: OAuth2 (delegated), JWT (stateless), API keys (service-to-service)
    - Rate limiting: Token bucket, fixed window, sliding window
    - Idempotency: Idempotency keys, conditional requests, safe retry
    
    **See references/ for deep dives**: `rest-patterns.md`, `graphql-patterns.md`, `grpc-patterns.md`, `versioning-strategies.md`, `authentication.md`
    
    ## Core Principles
    
    ### Universal API Design Standards
    
    Apply these principles across all API styles:
    
    **1. Consistency Over Cleverness**
    - Follow established conventions for your API style
    - Use predictable naming patterns (snake_case or camelCase, pick one)
    - Maintain consistent error response formats
    - Version breaking changes, never surprise clients
    
    **2. Design for Evolution**
    - Plan for versioning from day one
    - Use optional fields with sensible defaults
    - Deprecate gracefully with sunset dates
    - Document breaking vs non-breaking changes
    
    **3. Security by Default**
    - Require authentication unless explicitly public
    - Use HTTPS/TLS for all production endpoints
    - Implement rate limiting and throttling
    - Validate and sanitize all inputs
    - Return minimal error details to clients
    
    **4. Developer Experience First**
    - Provide comprehensive documentation (OpenAPI, GraphQL schema)
    - Return meaningful error messages with actionable guidance
    - Use standard HTTP status codes correctly
    - Include request IDs for debugging
    - Offer SDKs and code generators
    
    ## API Style Decision Tree
    
    ### When to Choose REST
    
    ✅ **Use REST when:**
    - Building CRUD-focused resource APIs
    - Clients need HTTP caching (ETags, Cache-Control)
    - Wide platform compatibility required (browsers, mobile, IoT)
    - Simple, stateless client-server model fits
    - Team familiar with HTTP/REST conventions
    
    ❌ **Avoid REST when:**
    - Complex data fetching with nested relationships (N+1 queries)
    - Real-time updates are primary use case
    - Need strong typing and code generation
    - High-performance RPC between microservices
    
    **Example Use Cases**: Public APIs, mobile backends, traditional web services
    
    ### When to Choose GraphQL
    
    ✅ **Use GraphQL when:**
    - Clients need flexible, client-driven queries
    - Complex data graphs with nested relationships
    - Multiple client types with different data needs
    - Real-time subscriptions required
    - Strong typing and schema validation needed
    
    ❌ **Avoid GraphQL when:**
    - Simple CRUD operations dominate
    - HTTP caching is critical (GraphQL uses POST)
    - File uploads are primary feature (requires extensions)
    - Team lacks GraphQL expertise
    - Performance optimization is complex (N+1 problem)
    
    **Example Use Cases**: Client-facing APIs, dashboards, mobile apps with varied UIs
    
    ### When to Choose gRPC
    
    ✅ **Use gRPC when:**
    - Microservice-to-microservice communication
    - High performance and low latency critical
    - Bidirectional streaming needed
    - Strong typing with Protocol Buffers
    - Polyglot environments (language interop)
    
    ❌ **Avoid gRPC when:**
    - Browser clients (limited support, needs grpc-web)
    - HTTP/JSON required for compatibility
    - Human-readable payloads preferred
    - Simple request/response patterns
    
    **Example Use Cases**: Internal microservices, streaming data, service mesh
    
    ## REST API Patterns
    
    ### Resource Naming
    
    ✅ **Good: Plural nouns, hierarchical**
    ```
    GET    /users              # List users
    GET    /users/123          # Get user
    POST   /users              # Create user
    PUT    /users/123          # Update user (full)
    PATCH  /users/123          # Update user (partial)
    DELETE /users/123          # Delete user
    GET    /users/123/orders   # User's orders (sub-resource)
    ```
    
    ❌ **Bad: Verbs, mixed conventions**
    ```
    GET    /getUsers           # Don't use verbs
    POST   /user/create        # Don't use verbs
    GET    /Users/123          # Don't capitalize
    GET    /user/123           # Don't mix singular/plural
    ```
    
    ### HTTP Status Codes
    
    **Success Codes**:
    - `200 OK`: Successful GET, PUT, PATCH, DELETE with body
    - `201 Created`: Successful POST, return Location header
    - `202 Accepted`: Async operation started
    - `204 No Content`: Successful DELETE, no body
    
    **Client Error Codes**:
    - `400 Bad Request`: Invalid input, validation error
    - `401 Unauthorized`: Missing or invalid authentication
    - `403 Forbidden`: Authenticated but insufficient permissions
    - `404 Not Found`: Resource doesn't exist
    - `409 Conflict`: State conflict (duplicate, version mismatch)
    - `422 Unprocessable Entity`: Semantic validation error
    - `429 Too Many Requests`: Rate limit exceeded
    
    **Server Error Codes**:
    - `500 Internal Server Error`: Unexpected error
    - `502 Bad Gateway`: Upstream service error
    - `503 Service Unavailable`: Temporary outage
    - `504 Gateway Timeout`: Upstream timeout
    
    ### Error Response Format
    
    ✅ **Consistent error structure**
    ```json
    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid request parameters",
        "details": [
          {
            "field": "email",
            "message": "Invalid email format",
            "code": "INVALID_FORMAT"
          }
        ],
        "request_id": "req_abc123",
        "documentation_url": "https://api.example.com/docs/errors/validation"
      }
    }
    ```
    
    ### Pagination Patterns
    
    **Offset Pagination** (simple, familiar):
    ```
    GET /users?limit=20&offset=40
    ```
    ✅ Use for: Small datasets, admin interfaces
    ❌ Avoid for: Large datasets (skips become expensive), real-time data
    
    **Cursor Pagination** (stable, efficient):
    ```
    GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
    Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }
    ```
    ✅ Use for: Infinite scroll, real-time feeds, large datasets
    ❌ Avoid for: Random access, page numbers
    
    **Keyset Pagination** (performant):
    ```
    GET /users?limit=20&after_id=123
    ```
    ✅ Use for: Ordered data, database index friendly
    ❌ Avoid for: Complex sorting, multiple sort keys
    
    See `references/rest-patterns.md` for filtering, sorting, field selection, HATEOAS
    
    ## GraphQL Patterns
    
    ### Schema Design
    
    ✅ **Good: Clear types, nullable by default**
    ```graphql
    type User {
      id: ID!                    # Non-null ID
      email: String!             # Required field
      name: String               # Optional (nullable by default)
      createdAt: DateTime!
      orders: [Order!]!          # Non-null array of non-null orders
    }
    
    type Query {
      user(id: ID!): User
      users(first: Int, after: String): UserConnection!
    }
    
    type Mutation {
      createUser(input: CreateUserInput!): CreateUserPayload!
    }
    
    input CreateUserInput {
      email: String!
      name: String
    }
    
    type CreateUserPayload {
      user: User
      userEdge: UserEdge
      errors: [UserError!]
    }
    ```
    
    ### Resolver Patterns
    
    **Avoid N+1 Queries with DataLoader**:
    ```typescript
    import DataLoader from 'dataloader';
    
    const userLoader = new DataLoader(async (userIds: string[]) => {
      const users = await db.users.findMany({ where: { id: { in: userIds } } });
      return userIds.map(id => users.find(u => u.id === id));
    });
    
    // Resolver batches queries automatically
    const resolvers = {
      Order: {
        user: (order) => userLoader.load(order.userId)
      }
    };
    ```
    
    ### Query Complexity Analysis
    
    Prevent expensive queries:
    ```typescript
    import { createComplexityLimitRule } from 'graphql-validation-complexity';
    
    const server = new ApolloServer({
      schema,
      validationRules: [
        createComplexityLimitRule(1000, {
          onCost: (cost) => console.log('Query cost:', cost),
        }),
      ],
    });
    ```
    
    See `references/graphql-patterns.md` for subscriptions, relay cursor connections, error handling
    
    ## gRPC Patterns
    
    ### Service Definition
    
    ```protobuf
    syntax = "proto3";
    
    package users.v1;
    
    service UserService {
      rpc GetUser (GetUserRequest) returns (User) {}
      rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
      rpc CreateUser (CreateUserRequest) returns (User) {}
      rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
      rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
    }
    
    message User {
      string id = 1;
      string email = 2;
      string name = 3;
      google.protobuf.Timestamp created_at = 4;
    }
    
    message GetUserRequest {
      string id = 1;
    }
    
    message ListUsersRequest {
      int32 page_size = 1;
      string page_token = 2;
    }
    
    message ListUsersResponse {
      repeated User users = 1;
      string next_page_token = 2;
    }
    ```
    
    ### Error Handling
    
    ```go
    import (
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
    )
    
    func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
        if req.Id == "" {
            return nil, status.Error(codes.InvalidArgument, "user ID is required")
        }
    
        user, err := s.db.GetUser(ctx, req.Id)
        if err != nil {
            if errors.Is(err, sql.ErrNoRows) {
                return nil, status.Error(codes.NotFound, "user not found")
            }
            return nil, status.Error(codes.Internal, "database error")
        }
    
        return user, nil
    }
    ```
    
    See `references/grpc-patterns.md` for streaming, interceptors, metadata, health checks
    
    ## Versioning Strategies
    
    ### URI Versioning (Simple, Explicit)
    
    ✅ **Most common, easy to understand**
    ```
    GET /v1/users/123
    GET /v2/users/123
    ```
    
    **Pros**: Clear, easy to route, browser-friendly
    **Cons**: Couples version to URL, duplicates routes
    
    ### Header Versioning (Clean URLs)
    
    ```
    GET /users/123
    Accept: application/vnd.myapi.v2+json
    ```
    
    **Pros**: Clean URLs, version separate from resource
    **Cons**: Less visible, harder to test manually
    
    ### Content Negotiation (Granular)
    
    ```
    GET /users/123
    Accept: application/vnd.myapi.user.v2+json
    ```
    
    **Pros**: Resource-level versioning, backward compatible
    **Cons**: Complex, harder to implement
    
    ### Version Deprecation Process
    
    ```json
    {
      "version": "1.0",
      "deprecated": true,
      "sunset_date": "2025-12-31",
      "migration_guide": "https://docs.api.com/v1-to-v2",
      "replacement_version": "2.0"
    }
    ```
    
    **Include deprecation warnings**:
    ```
    HTTP/1.1 200 OK
    Deprecation: true
    Sunset: Sat, 31 Dec 2025 23:59:59 GMT
    Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"
    ```
    
    See `references/versioning-strategies.md` for detailed migration patterns
    
    ## Authentication & Authorization
    
    ### OAuth 2.0 (Delegated Access)
    
    **Use for**: Third-party access, user consent, token refresh
    
    **Authorization Code Flow** (most secure for web/mobile):
    ```
    1. Client redirects to /authorize
    2. User authenticates, grants permissions
    3. Auth server redirects to callback with code
    4. Client exchanges code for access token
    5. Client uses access token for API requests
    ```
    
    ```http
    # Request token
    POST /oauth/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=AUTH_CODE
    &redirect_uri=https://client.com/callback
    &client_id=CLIENT_ID
    &client_secret=CLIENT_SECRET
    
    # Response
    {
      "access_token": "eyJhbGc...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
      "scope": "read write"
    }
    
    # Use token
    GET /v1/users/me
    Authorization: Bearer eyJhbGc...
    ```
    
    ### JWT (Stateless Auth)
    
    **Use for**: Microservices, stateless API auth, short-lived tokens
    
    ✅ **Good: Minimal claims, short expiry**
    ```json
    {
      "sub": "user_123",
      "iat": 1516239022,
      "exp": 1516242622,
      "scope": "read:users write:orders"
    }
    ```
    
    **Validation**:
    ```typescript
    import jwt from 'jsonwebtoken';
    
    const token = req.headers.authorization?.split(' ')[1];
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = payload.sub;
    ```
    
    ### API Keys (Service-to-Service)
    
    **Use for**: Server-to-server, CLI tools, webhooks
    
    ```http
    GET /v1/users
    X-API-Key: sk_live_abc123...
    
    # Or query parameter (less secure)
    GET /v1/users?api_key=sk_live_abc123
    ```
    
    **Key Practices**:
    - Prefix keys with environment (`sk_live_`, `sk_test_`)
    - Hash keys before storage (bcrypt, scrypt)
    - Allow key rotation without downtime
    - Support multiple keys per user
    - Rate limit per key
    
    See `references/authentication.md` for API key rotation, scopes, RBAC
    
    ## Rate Limiting
    
    ### Token Bucket (Burst-Friendly)
    
    ```
    Bucket: 100 tokens, refill 10/second
    Request costs 1 token
    Allows bursts up to bucket size
    ```
    
    **Headers**:
    ```http
    HTTP/1.1 200 OK
    X-RateLimit-Limit: 100
    X-RateLimit-Remaining: 73
    X-RateLimit-Reset: 1640995200
    ```
    
    **429 Response**:
    ```http
    HTTP/1.1 429 Too Many Requests
    Retry-After: 60
    X-RateLimit-Limit: 100
    X-RateLimit-Remaining: 0
    X-RateLimit-Reset: 1640995200
    
    {
      "error": {
        "code": "RATE_LIMIT_EXCEEDED",
        "message": "Rate limit exceeded. Try again in 60 seconds.",
        "limit": 100,
        "reset_at": "2025-01-01T00:00:00Z"
      }
    }
    ```
    
    ### Sliding Window (Fair Distribution)
    
    Counts requests in rolling time window. More accurate than fixed window.
    
    ### Per-User vs Per-IP
    
    - **Per-User**: Authenticated requests, fair quotas
    - **Per-IP**: Unauthenticated requests, prevent abuse
    - **Combined**: Both limits, take stricter
    
    ## Idempotency
    
    ### Idempotent Methods (HTTP Spec)
    
    **Naturally Idempotent**: GET, PUT, DELETE, HEAD, OPTIONS
    **Not Idempotent**: POST, PATCH
    
    ### Idempotency Keys
    
    Make POST requests idempotent:
    ```http
    POST /v1/payments
    Idempotency-Key: uuid-or-client-generated-key
    Content-Type: application/json
    
    {
      "amount": 1000,
      "currency": "USD",
      "customer": "cust_123"
    }
    ```
    
    **Server behavior**:
    1. First request: Process and store result with key
    2. Duplicate request (same key): Return stored result (200 or 201)
    3. Different request (same key): Return 409 Conflict
    
    **Implementation**:
    ```typescript
    const idempotencyKey = req.headers['idempotency-key'];
    if (idempotencyKey) {
      const cached = await redis.get(`idempotency:${idempotencyKey}`);
      if (cached) {
        return res.status(cached.status).json(cached.body);
      }
    }
    
    const result = await processPayment(req.body);
    await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
      status: 201,
      body: result
    });
    ```
    
    ### Conditional Requests
    
    Use ETags for safe updates:
    ```http
    # Get resource with ETag
    GET /v1/users/123
    Response: ETag: "abc123"
    
    # Update only if unchanged
    PUT /v1/users/123
    If-Match: "abc123"
    
    # 412 Precondition Failed if ETag changed
    ```
    
    ## Caching Strategies
    
    ### HTTP Caching Headers
    
    ```http
    # Public, cacheable for 1 hour
    Cache-Control: public, max-age=3600
    
    # Private (user-specific), revalidate
    Cache-Control: private, must-revalidate, max-age=0
    
    # No caching
    Cache-Control: no-store, no-cache, must-revalidate
    ```
    
    ### ETag Validation
    
    ```http
    # Server returns ETag
    GET /v1/users/123
    Response:
      ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
      Cache-Control: max-age=3600
    
    # Client conditional request
    GET /v1/users/123
    If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    
    # 304 Not Modified if unchanged (saves bandwidth)
    HTTP/1.1 304 Not Modified
    ```
    
    ### Last-Modified
    
    ```http
    GET /v1/users/123
    Response:
      Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
    
    # Conditional request
    GET /v1/users/123
    If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
    
    # 304 Not Modified if not modified
    ```
    
    ## Webhooks
    
    ### Event Delivery
    
    ```http
    POST https://client.com/webhooks/payments
    Content-Type: application/json
    X-Webhook-Signature: sha256=abc123...
    X-Webhook-Id: evt_abc123
    X-Webhook-Timestamp: 1640995200
    
    {
      "id": "evt_abc123",
      "type": "payment.succeeded",
      "created": 1640995200,
      "data": {
        "object": {
          "id": "pay_123",
          "amount": 1000,
          "status": "succeeded"
        }
      }
    }
    ```
    
    ### Signature Verification
    
    ```typescript
    import crypto from 'crypto';
    
    function verifyWebhookSignature(
      payload: string,
      signature: string,
      secret: string
    ): boolean {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(`sha256=${expectedSignature}`)
      );
    }
    ```
    
    ### Retry Strategy
    
    - **Exponential backoff**: 1s, 2s, 4s, 8s, 16s, 32s, 64s
    - **Timeout**: 5-30 seconds per attempt
    - **Max attempts**: 3-7 attempts
    - **Dead letter queue**: Store failed events
    - **Manual retry**: UI for re-sending failed events
    
    ## API Documentation
    
    ### OpenAPI/Swagger (REST)
    
    ```yaml
    openapi: 3.0.0
    info:
      title: User API
      version: 1.0.0
    paths:
      /users/{id}:
        get:
          summary: Get user by ID
          parameters:
            - name: id
              in: path
              required: true
              schema:
                type: string
          responses:
            '200':
              description: Successful response
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/User'
            '404':
              description: User not found
    components:
      schemas:
        User:
          type: object
          required: [id, email]
          properties:
            id:
              type: string
            email:
              type: string
              format: email
            name:
              type: string
    ```
    
    ### GraphQL Schema (Self-Documenting)
    
    GraphQL introspection provides automatic documentation. Use descriptions:
    
    ```graphql
    """
    Represents a user account in the system.
    Created via the createUser mutation.
    """
    type User {
      """Unique identifier for the user"""
      id: ID!
    
      """Email address, must be unique"""
      email: String!
    
      """Optional display name"""
      name: String
    }
    ```
    
    ### API Documentation Best Practices
    
    1. **Interactive examples**: Provide working code samples
    2. **Authentication guide**: Step-by-step auth setup
    3. **Error catalog**: Document all error codes with examples
    4. **Rate limits**: Clearly state limits and headers
    5. **Changelog**: Track breaking and non-breaking changes
    6. **Migration guides**: Version upgrade instructions
    7. **SDKs**: Provide client libraries for popular languages
    
    ## Anti-Patterns
    
    ❌ **Over-fetching (REST)**: Returning entire objects when fields are unused
    ✅ **Solution**: Support field selection (`?fields=id,name,email`)
    
    ❌ **Under-fetching (REST)**: Requiring multiple requests for related data
    ✅ **Solution**: Support expansion (`?expand=orders,profile`) or use GraphQL
    
    ❌ **Chatty APIs**: Too many round-trips for common operations
    ✅ **Solution**: Batch endpoints, compound documents, or GraphQL
    
    ❌ **Ignoring HTTP semantics**: Using GET for mutations, wrong status codes
    ✅ **Solution**: Follow HTTP spec, use correct methods and status codes
    
    ❌ **Exposing internal structure**: URLs/schemas mirror database
    ✅ **Solution**: Design resource-oriented APIs independent of storage
    
    ❌ **Missing versioning**: Breaking changes without version increments
    ✅ **Solution**: Version from day one, never break existing versions
    
    ❌ **Poor error messages**: Generic "An error occurred"
    ✅ **Solution**: Specific, actionable error messages with codes
    
    ❌ **No rate limiting**: APIs vulnerable to abuse
    ✅ **Solution**: Implement rate limiting from the start
    
    ## Testing Strategies
    
    ### Contract Testing
    
    ```typescript
    // Pact contract test
    import { PactV3 } from '@pact-foundation/pact';
    
    const provider = new PactV3({
      consumer: 'FrontendApp',
      provider: 'UserAPI'
    });
    
    it('gets a user by ID', () => {
      provider
        .given('user 123 exists')
        .uponReceiving('a request for user 123')
        .withRequest({
          method: 'GET',
          path: '/users/123'
        })
        .willRespondWith({
          status: 200,
          body: { id: '123', email: 'user@example.com' }
        });
    });
    ```
    
    ### Load Testing
    
    ```javascript
    // k6 load test
    import http from 'k6/http';
    import { check } from 'k6';
    
    export const options = {
      stages: [
        { duration: '30s', target: 20 },
        { duration: '1m', target: 20 },
        { duration: '10s', target: 0 }
      ],
      thresholds: {
        http_req_duration: ['p(95)<500'], // 95% under 500ms
        http_req_failed: ['rate<0.01']    // <1% errors
      }
    };
    
    export default function () {
      const res = http.get('https://api.example.com/users');
      check(res, {
        'status is 200': (r) => r.status === 200,
        'response time < 500ms': (r) => r.timings.duration < 500
      });
    }
    ```
    
    ## Related Skills
    
    - **graphql**: Deep GraphQL schema design, resolvers, Apollo Server
    - **typescript**: Type-safe API clients and servers
    - **nodejs-backend**: Express/Fastify REST API implementation
    - **django**: Django REST Framework patterns
    - **fastapi**: FastAPI Python REST/GraphQL APIs
    - **flask**: Flask-RESTful patterns
    
    ## References
    
    - **rest-patterns.md**: Deep REST coverage (HATEOAS, filtering, field selection)
    - **graphql-patterns.md**: GraphQL subscriptions, relay cursor connections, federation
    - **grpc-patterns.md**: Streaming patterns, interceptors, service mesh integration
    - **versioning-strategies.md**: Detailed versioning approaches and migration patterns
    - **authentication.md**: OAuth flows, JWT best practices, API key rotation, RBAC
    
    ## Additional Resources
    
    - [REST API Design Rulebook](https://www.oreilly.com/library/view/rest-api-design/9781449317904/) - O'Reilly REST guide
    - [GraphQL Best Practices](https://graphql.org/learn/best-practices/) - Official GraphQL guide
    - [gRPC Best Practices](https://grpc.io/docs/guides/performance/) - Official gRPC guide
    - [RFC 7807: Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807) - Standard error format
    - [OpenAPI Specification](https://spec.openapis.org/oas/latest.html) - REST documentation standard
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related