REST API Design Principles That Scale

· 11 min read · Backend Development

Practical REST API design covering URL structure, response formats, status codes, versioning, pagination, and rate limiting.

REST API Design Principles That Scale

Your API starts with 5 endpoints. Then it grows to 50. Then 200. At some point, the naming is inconsistent, the error responses vary by endpoint, and new developers spend more time reading the API docs than writing code. Good API design prevents this entropy.

URL Structure

Resources are nouns, not verbs:

# GOOD — resources
GET    /api/v1/users
GET    /api/v1/users/{id}
POST   /api/v1/users
PATCH  /api/v1/users/{id}
DELETE /api/v1/users/{id}

# BAD — verbs
GET    /api/v1/getUsers
POST   /api/v1/createUser
POST   /api/v1/deleteUser/{id}

Nested resources for relationships:

GET /api/v1/users/{id}/posts        # Posts by user
GET /api/v1/posts/{id}/comments     # Comments on post

Limit nesting to one level. /users/{id}/posts/{id}/comments/{id}/likes is too deep — flatten to /comments/{id}/likes.

Consistent Response Format

Every endpoint returns the same structure:

{
  "data": { ... },
  "meta": {
    "timestamp": "2024-01-15T12:00:00Z",
    "request_id": "req_abc123"
  }
}

For collections:

{
  "data": [ ... ],
  "meta": {
    "total": 156,
    "page": 1,
    "per_page": 20,
    "total_pages": 8
  }
}

For errors:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "field": "email", "message": "Must be a valid email address" }
    ]
  },
  "meta": {
    "timestamp": "2024-01-15T12:00:00Z",
    "request_id": "req_abc123"
  }
}

Consistent structure means the frontend can write a single API client that handles all responses.

HTTP Status Codes

Use the correct status codes:

Code Meaning When to Use
200 OK Successful GET, PATCH
201 Created Successful POST that creates a resource
204 No Content Successful DELETE
400 Bad Request Validation error, malformed input
401 Unauthorized Missing or invalid auth token
403 Forbidden Valid auth but insufficient permissions
404 Not Found Resource does not exist
409 Conflict Duplicate resource (e.g., email already exists)
422 Unprocessable Syntactically correct but semantically invalid
429 Too Many Requests Rate limit exceeded
500 Internal Error Unexpected server failure

Versioning

Prefix all endpoints with a version:

/api/v1/users
/api/v2/users

When you need breaking changes, create a new version. The old version continues to work. Give clients 6 months to migrate with deprecation headers:

Deprecation: true
Sunset: Sat, 01 Jul 2025 00:00:00 GMT
Link: </api/v2/users>; rel="successor-version"

Pagination

Use cursor-based pagination for consistency:

GET /api/v1/posts?cursor=post_abc123&limit=20

Response:

{
  "data": [...],
  "meta": {
    "next_cursor": "post_xyz789",
    "has_more": true
  }
}

Cursor pagination is stable — inserts and deletes do not cause skipped or duplicated items, unlike offset pagination.

Filtering and Sorting

GET /api/v1/posts?category=backend&published=true&sort=-created_at&limit=20

Conventions:

  • Filters as query params: field=value
  • Sort with prefix: -created_at (descending), created_at (ascending)
  • Limit for page size: limit=20
  • Fields for sparse responses: fields=id,title,slug

Rate Limiting

Include rate limit info in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705315200

PATCH vs. PUT

PUT replaces the entire resource. If you PUT without a field, it is set to null.

PATCH updates only the provided fields. Missing fields remain unchanged.

Use PATCH for updates in most cases. PUT is for complete replacements.

Takeaways

Good API design is about consistency and predictability. When every endpoint follows the same URL structure, returns the same response format, and uses the correct status codes, developers can use the API without memorizing endpoint-specific quirks.

Design your API for the next developer, not just the current feature.