JustinFirsching avatar

api-design

REST API design patterns for endpoints, responses, errors, and conventions

by JustinFirsching|Open Source

API Design Skill

Use this skill when designing, implementing, or reviewing REST APIs.

URL Structure

Resources

  • Use nouns, not verbs: /users not /getUsers
  • Use plural names: /users, /orders, /items
  • Use lowercase with hyphens: /order-items not /orderItems
  • Nest for relationships: /users/{id}/orders

Examples

GET    /users              # List users
POST   /users              # Create user
GET    /users/{id}         # Get user
PUT    /users/{id}         # Replace user
PATCH  /users/{id}         # Update user fields
DELETE /users/{id}         # Delete user

GET    /users/{id}/orders  # List user's orders
POST   /users/{id}/orders  # Create order for user

Anti-patterns

# Bad
GET  /getUser?id=123
POST /createUser
GET  /users/delete/123
POST /users/123/updateEmail

# Good
GET    /users/123
POST   /users
DELETE /users/123
PATCH  /users/123

HTTP Methods

MethodPurposeIdempotentSafe
GETRetrieve resource(s)YesYes
POSTCreate resourceNoNo
PUTReplace resource entirelyYesNo
PATCHPartial updateYesNo
DELETERemove resourceYesNo

Idempotency

Idempotent operations produce the same result when called multiple times.

  • PUT with same data = same result
  • DELETE on deleted resource = still deleted (or 404)
  • POST creates new resource each time (not idempotent)

Status Codes

Success (2xx)

CodeMeaningUse Case
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (include Location header)
204No ContentSuccessful DELETE, or PUT with no response body

Client Errors (4xx)

CodeMeaningUse Case
400Bad RequestInvalid syntax, validation errors
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn't exist
405Method Not AllowedWrong HTTP method
409ConflictState conflict (duplicate, version mismatch)
422Unprocessable EntitySemantic errors (valid syntax, invalid data)
429Too Many RequestsRate limit exceeded

Server Errors (5xx)

CodeMeaningUse Case
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service error
503Service UnavailableTemporarily unavailable
504Gateway TimeoutUpstream timeout

Error Responses

Use a consistent error format across all endpoints.

Standard Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid fields.",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Must be a positive integer"
      }
    ]
  }
}

Principles

  • Human-readable message for display
  • Machine-readable code for programmatic handling
  • details array for field-level errors
  • Never expose stack traces or internal details in production
  • Log detailed errors server-side with correlation ID

Request/Response Design

Field Naming

Choose one convention and be consistent:

  • snake_case: Python/Ruby convention, common in REST
  • camelCase: JavaScript convention
// Consistent snake_case
{
  "user_id": 123,
  "created_at": "2024-01-15T10:30:00Z",
  "email_verified": true
}

Timestamps

Use ISO 8601 format with timezone:

{
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-15T14:22:33Z"
}

Null vs Absent

  • Absent field = not requested or not applicable
  • Null = explicitly no value
  • Document which fields can be null

IDs

  • Use strings for IDs in JSON (avoids JavaScript integer limits)
  • Keep internal ID format consistent (UUID, auto-increment, etc.)

Pagination

Offset-based (Simple)

GET /users?limit=20&offset=40

Response:

{
  "data": [...],
  "pagination": {
    "total": 150,
    "limit": 20,
    "offset": 40
  }
}

Cursor-based (Scalable)

GET /users?limit=20&cursor=eyJpZCI6MTIzfQ

Response:

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

Cursor-based is better for large datasets and real-time data.

Filtering & Sorting

Filtering

GET /users?status=active&role=admin
GET /orders?created_after=2024-01-01&status=pending

Sorting

GET /users?sort=created_at        # Ascending
GET /users?sort=-created_at       # Descending (prefix with -)
GET /users?sort=last_name,first_name  # Multiple fields

Versioning

Always use URL path versioning.

GET /v1/users
GET /v2/users

Benefits:

  • Simple and explicit
  • Easy to route and test
  • Visible in logs and debugging
  • No header parsing required
  • Works with all HTTP clients

Do NOT use header-based versioning (Accept: application/vnd.api+json; version=2) - it's harder to test, debug, and maintain.

Authentication

Bearer Token

Authorization: Bearer <token>

API Key

X-API-Key: <key>
# or
Authorization: ApiKey <key>

Response to Auth Failures

  • 401 for missing/invalid credentials
  • 403 for valid credentials but insufficient permissions

Rate Limiting

Include headers in responses:

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

When exceeded, return 429:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please retry after 60 seconds.",
    "retry_after": 60
  }
}

Health Check Endpoints

Implement health checks for monitoring and orchestration:

GET /health          # Basic liveness check
GET /health/ready    # Readiness (can accept traffic?)
GET /health/live     # Liveness (is the process healthy?)

Response Format

{
  "status": "healthy",
  "timestamp": "2024-01-15T10:30:00Z",
  "checks": {
    "database": { "status": "healthy", "latency_ms": 5 },
    "cache": { "status": "healthy", "latency_ms": 1 },
    "external_api": { "status": "degraded", "latency_ms": 500 }
  }
}

Status Values

  • healthy - All systems operational
  • degraded - Operating with reduced functionality
  • unhealthy - Critical failure, should not receive traffic

Request Tracing

Include request IDs for debugging and correlation:

Headers

X-Request-Id: 550e8400-e29b-41d4-a716-446655440000

Implementation

  • Generate UUID if client doesn't provide one
  • Include in all log entries for this request
  • Return in response headers
  • Pass to downstream services
  • Include in error responses
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred.",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Long-Running Operations

For operations that take more than a few seconds, use async patterns:

Request

POST /v1/reports
{
  "type": "annual_summary",
  "year": 2024
}

Response (202 Accepted)

{
  "job_id": "abc123",
  "status": "pending",
  "status_url": "/v1/jobs/abc123",
  "estimated_completion": "2024-01-15T10:35:00Z"
}

Polling Status

GET /v1/jobs/abc123
{
  "job_id": "abc123",
  "status": "completed",
  "result_url": "/v1/reports/xyz789",
  "completed_at": "2024-01-15T10:33:00Z"
}

Bulk Operations

For operations on multiple resources:

POST   /v1/users/bulk      # Create multiple
PATCH  /v1/users/bulk      # Update multiple
DELETE /v1/users/bulk      # Delete multiple

Request Format

{
  "operations": [
    { "method": "create", "data": { "name": "Alice" } },
    { "method": "update", "id": "123", "data": { "name": "Bob" } },
    { "method": "delete", "id": "456" }
  ]
}

Response Format

{
  "results": [
    { "status": "success", "id": "789" },
    { "status": "success", "id": "123" },
    { "status": "error", "id": "456", "error": { "code": "NOT_FOUND" } }
  ],
  "summary": { "succeeded": 2, "failed": 1 }
}

API Design Checklist

  • Resources use nouns, not verbs
  • HTTP methods match semantics (GET for read, POST for create, etc.)
  • Status codes are correct and consistent
  • Error format is consistent across all endpoints
  • Pagination is implemented for list endpoints
  • Authentication is documented and enforced
  • Rate limiting is in place for public endpoints
  • API is versioned (URL path: /v1/)
  • Request/response examples are documented
  • Breaking changes are avoided or versioned
  • Health check endpoints are implemented
  • Request tracing is in place