DEV Community

Alex Chen
Alex Chen

Posted on

REST API Design: Building APIs Developers Love (2026)

REST API Design: Building APIs Developers Love (2026)

A good API is like a good UI — intuitive, consistent, and a pleasure to use.

Core Principles

1. Consistency over cleverness
   → Same patterns everywhere
   → Predictable response shapes
   → No surprises

2. Resources, not actions
   → GET /users (not /getUsers)
   → POST /users (not /createUser)
   → Nouns, not verbs

3. Statelessness
   → Each request contains everything needed
   → Server doesn't store session state between requests
   → Scales horizontally

4. Practical defaults
   → JSON for request/response bodies
   → camelCase for JSON keys (JavaScript convention)
   → snake_case for URL parameters/headers (HTTP convention)
Enter fullscreen mode Exit fullscreen mode

URL Design

# Good: Resource-oriented, hierarchical

GET     /api/users                    # List all users
GET     /api/users?role=admin&sort=-createdAt  # Filtered/sorted list
POST    /api/users                    # Create user
GET     /api/users/:id                # Get specific user
PUT     /api/users/:id                # Full update
PATCH   /api/users/:id                # Partial update
DELETE  /api/users/:id                # Delete user

# Nested resources (relationships)
GET     /api/users/:id/orders         # User's orders
POST    /api/users/:id/orders         # Create order for user
GET     /api/users/:id/orders/:orderId  # Specific order

# Actions on resources (when CRUD doesn't fit)
POST    /api/users/:id/activate       # Activate account
POST    /api/users/:id/deactivate     # Deactivate
POST    /api/orders/:id/cancel        # Cancel order
POST    /api/orders/:id/refund        # Request refund

# Search (complex queries)
GET     /api/search?q=keyword&type=users
POST    /api/search                   # For complex filters (body > URL length)

# Bad URLs to avoid:
❌ /api/getAllUsers               # Verb in path
❌ /api/user-data                 # Inconsistent naming
❌ /api/users/1                   # Use :id pattern consistently
❌ /api/getUserById?id=1          # Query params for required IDs
❌ /api/v2/new-users              # Version in weird places
Enter fullscreen mode Exit fullscreen mode

HTTP Methods: The Standard Mapping

Method Safe Idempotent Meaning Example
GET ✅ ✅ Read resource Get user profile
HEAD ✅ ✅ Like GET, no body Check if exists
POST ❌ ❌ Create resource New user
PUT ❌ ✅ Replace entirely Update full profile
PATCH ❌ ❌ Partial update Change email only
DELETE ❌ ❌ Remove resource Delete account
OPTIONS ✅ ✅ CORS preflight Browser auto-sends

Key concepts:

  • Safe: Doesn't modify server state (GET is safe)
  • Idempotent: Same request = same result (PUT same data twice = identical outcome; POST creates TWO resources)

Response Format

Success Responses

// Single resource
{
  "success": true,
  "data": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": "Alice",
    "role": "user",
    "createdAt": "2026-05-27T10:33:00Z",
    "updatedAt": "2026-05-27T10:33:00Z"
  }
}

// Collection with pagination
{
  "success": true,
  "data": [
    { "id": "usr_001", "name": "Alice" },
    { "id": "usr_002", "name": "Bob" }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": false
  }
}

// Created (201)
{
  "success": true,
  "data": { "id": "usr_new123", ... },
  "message": "User created successfully"
}

// No content (204)
// Response body is empty (for successful DELETE)
Enter fullscreen mode Exit fullscreen mode

Error Responses

// Client error (4xx)
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "email", "issue": "Invalid email format" },
      { "field": "password", "issue": "Must be at least 8 characters" }
    ],
    "requestId": "req_abc123"
  }
}

// Not found (404)
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found (id: usr_xyz)",
    "requestId": "req_abc123"
  }
}

// Server error (500)
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An internal error occurred",
    "requestId": "req_abc123"
    // Never expose stack traces or internals in production!
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Code Standards

// Use consistent, machine-readable error codes:
const ERROR_CODES = {
  // Client errors (4xx)
  BAD_REQUEST: 'BAD_REQUEST',
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  NOT_ACCEPTABLE: 'NOT_ACCEPTABLE',           // 406
  CONFLICT: 'CONFLICT',                       // 409
  VALIDATION_ERROR: 'VALIDATION_ERROR',        // 422
  RATE_LIMITED: 'RATE_LIMITED',                // 429

  // Server errors (5xx)
  INTERNAL_ERROR: 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', // 503
  TIMEOUT: 'TIMEOUT',                          // 504 (gateway timeout)
};
Enter fullscreen mode Exit fullscreen mode

Query Parameters & Filtering

// Standard pagination
GET /api/users?page=2&limit=20
// page starts at 1, default limit = 20

// Sorting
GET /api/users?sort=createdAt          // Ascending
GET /api/users?sort=-createdAt         // Descending (- prefix)
GET /api/users?sort=name,-createdAt   // Multi-sort

// Filtering
GET /api/users?role=admin             // Exact match
GET /api/users?status=active,pending  // Multiple values (OR)
GET /api/users?minAge=18&maxAge=65     // Range filter
GET /api/users?search=alice            // Full-text search
GET /api/users?tags=javascript,node   // Has ALL tags (AND)

// Field selection (reduce payload)
GET /api/users?fields=id,name,email    // Only return these fields
GET /api/users?exclude=passwordHash,lastLogin  // Exclude sensitive fields

// Include related resources
GET /api/users/:id?include=orders,profile  // Eager load relations

// Server-side implementation:
function parseQuery(req) {
  return {
    page: Math.max(1, parseInt(req.query.page) || 1),
    limit: Math.min(100, parseInt(req.query.limit) || 20), // Cap at 100
    sort: req.query.sort || '-createdAt',
    search: req.query.search,
    fields: req.query.fields?.split(','),
    include: req.query.include?.split(','),
    // ... build filters from remaining query params
  };
}
Enter fullscreen mode Exit fullscreen mode

Versioning Your API

Three approaches:

1. URL Path Versioning (most common)
   /api/v1/users
   /api/v2/users
   Pros: Clear, works with CDNs, easy to deprecate
   Cons: URL clutter

2. Header Versioning
   Accept: application/vnd.api.v2+json
   Pros: Clean URLs
   Cons: Harder to debug in browser, not obvious

3. No Versioning (just evolve)
   /api/users
   Pros: Simplest
   Cons: Breaking changes break clients

My recommendation: Start with URL versioning.
Only add v1 when you have real users and need stability guarantees.
Enter fullscreen mode Exit fullscreen mode

Authentication Patterns

// Pattern 1: Bearer Token (JWT) — Most common for APIs
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// Pattern 2: API Key — For server-to-server or public APIs
X-API-Key: sk_live_abc123
// Or query param: ?api_key=sk_live_abc123 (less secure but simpler)

// Pattern 3: OAuth 2.0 — For third-party access
Authorization: Bearer oauth_access_token_here

// Response should include auth info when relevant:
{
  "data": { /* ... */ },
  "meta": {
    "scopes": ["read:profile", "write:profile"],
    "rateLimit": { "remaining": 95, "reset": "2026-05-27T11:00:00Z" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting Done Right

// Headers on EVERY response (even errors!):
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1716825600      // Unix timestamp
Retry-After: 60                      // On 429 responses

// Rate limit tiers (common pattern):
// Unauthenticated: 30/hour
// Authenticated (free): 100/hour
// Paid tier: 1000/hour
// Admin: unlimited

// Always include rate limit info in response body on 429:
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please try again later.",
    "retryAfter": 45,
    "rateLimit": {
      "limit": 100,
      "remaining": 0,
      "resetAt": "2026-05-27T11:00:00Z"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Documentation

Your API is only as good as its documentation.

Minimum requirements:
→ Every endpoint documented
→ Request/response examples for each
→ Authentication explanation
→ Error codes reference
→ Rate limits stated
→ SDK/client library examples

Tools I recommend:
→ OpenAPI/Swagger (auto-generate interactive docs)
→ Postman Collections (shareable, importable)
→ README.md with quick-start guide (always!)

The best API docs let developers succeed WITHOUT contacting you.
Enter fullscreen mode Exit fullscreen mode

Quick Checklist

Before shipping any endpoint:

□ Uses correct HTTP method (GET/POST/PATCH/PUT/DELETE)
□ URL follows resource-naming convention
□ Returns consistent response shape ({ success, data/error })
□ Handles pagination for collections
□ Validates input and returns 422 with details
□ Returns 404 (not 500) for missing resources
□ Includes rate limit headers
□ Has authentication where needed
□ Returns appropriate status codes
□ Documented with examples
□ Error messages are helpful (not exposing internals)
□ Request ID included for support debugging
□ Fields use consistent naming (camelCase in JSON body)
Enter fullscreen mode Exit fullscreen mode

What's the best/worst API you've worked with?

Follow @armorbreak for more practical developer guides.

Top comments (0)