Back to Blog
Backend

REST API Design: Principles and Patterns

I've designed and consumed hundreds of APIs over my career. Some were a joy to work with—intuitive, consistent, well-documented. Others made me question my career choices. Let's talk about what separates the two.

REST is a Contract, Not Just an Architecture

When you design an API, you're creating a contract with every developer who will ever use it. Break that contract—with inconsistent naming, unexpected behaviors, or poor error handling—and you'll spend more time answering support questions than building features.

The best APIs are boring. They're predictable. They do exactly what you expect them to do.

1. Resource Naming: The Foundation

URLs should represent resources (nouns), not actions (verbs). The HTTP method tells you the action.

// Bad - verbs in URLs
POST /createUser
GET /getUserById/123
POST /deleteUser/123

// Good - nouns with proper HTTP methods
POST   /users          → Create a user
GET    /users/123      → Get user 123
PUT    /users/123      → Update user 123
DELETE /users/123      → Delete user 123

Plural vs Singular

Pick one and stick with it. I prefer plural because it's more consistent:

GET  /users           → List all users
GET  /users/123       → Get specific user
POST /users           → Create a user
GET  /users/123/posts → Get posts for user 123

Nested Resources

Keep nesting shallow. More than two levels deep becomes confusing:

// Good - shallow nesting
GET /users/123/posts
GET /posts/456/comments

// Avoid - too deep
GET /users/123/posts/456/comments/789/replies

2. HTTP Methods: Use Them Correctly

Method Purpose Idempotent? Safe?
GET Retrieve resource(s) Yes Yes
POST Create a new resource No No
PUT Replace entire resource Yes No
PATCH Partial update No* No
DELETE Remove resource Yes No
💡 Idempotency Matters

Idempotent operations can be safely retried. If a network error occurs during a PUT request, the client can retry without fear of creating duplicate data. This is crucial for building reliable systems.

3. Status Codes: Tell the Client What Happened

Don't return 200 OK with an error message in the body. Use status codes properly:

// Success codes
200 OK           → Request succeeded
201 Created      → Resource created (return the new resource)
204 No Content   → Success, nothing to return (good for DELETE)

// Client errors
400 Bad Request  → Malformed request syntax
401 Unauthorized → Authentication required
403 Forbidden    → Authenticated but not authorized
404 Not Found    → Resource doesn't exist
409 Conflict     → Resource conflict (e.g., duplicate email)
422 Unprocessable→ Validation errors

// Server errors
500 Internal     → Something broke on our end
503 Unavailable  → Service temporarily down

4. Error Handling: Be Helpful

When things go wrong, give developers enough information to fix the problem:

// Bad - unhelpful error
{
    "error": "Invalid request"
}

// Good - actionable error response
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "The request contains invalid data",
        "details": [
            {
                "field": "email",
                "message": "Must be a valid email address",
                "received": "not-an-email"
            },
            {
                "field": "age",
                "message": "Must be at least 18",
                "received": 16
            }
        ],
        "documentation": "https://api.example.com/docs/errors#VALIDATION_ERROR"
    }
}

5. Pagination: Don't Return Everything

Never return unbounded lists. A table with a million rows will crash your server and the client.

// Cursor-based pagination (preferred)
GET /posts?cursor=abc123&limit=20

{
    "data": [...],
    "pagination": {
        "next_cursor": "def456",
        "has_more": true
    }
}

// Offset-based pagination (simpler but less efficient)
GET /posts?page=3&per_page=20

{
    "data": [...],
    "pagination": {
        "current_page": 3,
        "per_page": 20,
        "total_pages": 50,
        "total_items": 1000
    }
}
💡 Why Cursor Pagination?

Offset pagination breaks when data changes between requests. If new items are added, users might see duplicates or miss items. Cursor pagination is stable regardless of data changes.

6. Filtering, Sorting, and Searching

Use query parameters for filtering and sorting:

// Filtering
GET /posts?status=published&author_id=123

// Sorting
GET /posts?sort=-created_at,title  // "-" prefix for descending

// Searching
GET /posts?search=javascript

// Combined
GET /posts?status=published&sort=-created_at&search=api&limit=20

7. Versioning: Plan for Change

Your API will change. Plan for it from day one:

// URL versioning (most common)
GET /v1/users
GET /v2/users

// Header versioning (cleaner URLs)
GET /users
Accept: application/vnd.myapi.v2+json

My preference is URL versioning—it's explicit, easy to understand, and works in browser address bars.

8. Security Essentials

// Rate limit headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

// When rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60

9. Documentation: Your API's First Impression

If your API isn't documented, it doesn't exist. Use OpenAPI (Swagger) specification:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0

paths:
  /users:
    post:
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
      responses:
        '201':
          description: User created successfully

Real-World Design Checklist

  1. ✅ Resource names are plural nouns
  2. ✅ HTTP methods match their semantic meaning
  3. ✅ Status codes accurately reflect outcomes
  4. ✅ Errors include actionable information
  5. ✅ Pagination is implemented for list endpoints
  6. ✅ Filtering and sorting use query parameters
  7. ✅ API is versioned from day one
  8. ✅ Authentication is implemented
  9. ✅ Rate limiting is in place
  10. ✅ OpenAPI documentation is up to date

Conclusion

Great API design is about empathy. Think about the developer who will consume your API at 2 AM trying to ship a feature. Make their life easy with consistent patterns, clear errors, and comprehensive documentation.

Your API is a product. Treat it like one.