REST API Design Best Practices: The Complete Guide

A well-designed API is a joy to work with. A poorly designed one creates bugs, confusion, and support tickets for years. This guide covers the patterns that the best APIs in production follow.

1. URL Structure

Use nouns, not verbs

URLs represent resources (things), not actions. The HTTP method communicates the action.

# Good - nouns
GET    /api/users
POST   /api/users
GET    /api/users/123
PUT    /api/users/123
DELETE /api/users/123

# Bad - verbs in URLs
GET    /api/getUsers
POST   /api/createUser
POST   /api/deleteUser/123

Use plural nouns

Stay consistent. /users for the collection, /users/123 for a single resource.

Nest for relationships (but keep it shallow)

# Good - one level of nesting
GET /api/users/123/orders
GET /api/users/123/orders/456

# Bad - too deep
GET /api/users/123/orders/456/items/789/reviews
# Better: flatten with query params or separate endpoint
GET /api/order-items/789/reviews

Use kebab-case

# Good
/api/user-profiles
/api/order-items

# Bad
/api/userProfiles
/api/order_items

2. HTTP Methods

MethodPurposeIdempotent?Request Body?
GETRead a resourceYesNo
POSTCreate a resourceNoYes
PUTReplace a resource entirelyYesYes
PATCHPartially update a resourceYes*Yes
DELETERemove a resourceYesNo

PUT vs PATCH:PUT replaces the entire resource (send all fields). PATCH updates only the fields you send. In practice, most "update" endpoints use PATCH because clients rarely want to resend unchanged fields.

// PUT - full replacement (must send everything)
PUT /api/users/123
{
  "name": "Alice",
  "email": "[email protected]",
  "role": "admin",
  "avatar": "https://..."
}

// PATCH - partial update (only changed fields)
PATCH /api/users/123
{
  "name": "Alice Updated"
}

3. Status Codes

Use the right status code. Clients depend on them for control flow.

Success (2xx)

200 OK              — General success (GET, PUT, PATCH)
201 Created         — Resource created (POST). Include Location header.
204 No Content      — Success with no body (DELETE)

Client Errors (4xx)

400 Bad Request     — Malformed request, validation error
401 Unauthorized    — No auth credentials / invalid token
403 Forbidden       — Authenticated but not authorized
404 Not Found       — Resource doesn't exist
405 Method Not Allowed — Wrong HTTP method for this endpoint
409 Conflict        — Duplicate resource, version conflict
422 Unprocessable   — Valid JSON but semantically invalid
429 Too Many Requests — Rate limit exceeded

Server Errors (5xx)

500 Internal Server Error — Unexpected server failure
502 Bad Gateway     — Upstream service error
503 Service Unavailable — Temporarily down (maintenance)
504 Gateway Timeout — Upstream service timeout

Rule of thumb: If the client can fix the problem by changing the request, use 4xx. If the server has a bug or is overloaded, use 5xx.

4. Error Handling

Consistent error responses are critical. Every error should use the same shape.

// Standard error response structure
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address"
      },
      {
        "field": "name",
        "message": "Must be between 2 and 100 characters"
      }
    ]
  }
}

// 404 error
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User with id 999 not found"
  }
}

// 401 error
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired access token"
  }
}

Never expose stack traces, SQL queries, or internal implementation details in production error responses.

5. Pagination

Every list endpoint must be paginated. Returning unbounded results is a ticking time bomb.

Offset-based (simple, most common)

GET /api/users?page=2&limit=20

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 156,
    "totalPages": 8
  }
}

Cursor-based (better for real-time data)

GET /api/feed?cursor=eyJpZCI6MTIzfQ&limit=20

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6MTQzfQ",
    "hasMore": true
  }
}

When to use which: Offset-based is simpler and allows jumping to arbitrary pages. Cursor-based is more performant on large datasets and handles real-time insertions/deletions without skipping or duplicating items.

6. Filtering, Sorting & Field Selection

Filtering

# Simple equality
GET /api/users?role=admin&status=active

# Operators for ranges
GET /api/orders?created_after=2026-01-01&total_min=100

# Search
GET /api/users?q=alice

Sorting

# Sort ascending
GET /api/users?sort=name

# Sort descending (prefix with -)
GET /api/users?sort=-created_at

# Multiple sort fields
GET /api/users?sort=-created_at,name

Field selection (sparse fieldsets)

# Only return specific fields
GET /api/users?fields=id,name,email

# Reduces payload size and database load

7. Versioning

APIs evolve. You need a strategy for breaking changes that does not break existing clients.

URL path versioning (most common, most explicit)

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

Header versioning

GET /api/users
Accept: application/vnd.myapp.v2+json

Query parameter versioning

GET /api/users?version=2

Recommendation: Use URL path versioning. It is the easiest to discover, test in a browser, and route at the infrastructure level. Only bump the version for breaking changes.

8. Authentication Patterns

Bearer tokens (JWT or opaque)

GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

API keys (for server-to-server)

GET /api/data
X-API-Key: sk_live_abc123...

OAuth 2.0 flow summary

1. Client redirects user to:
   GET /oauth/authorize?client_id=...&redirect_uri=...&scope=read

2. User approves, redirect back with code:
   GET /callback?code=abc123

3. Client exchanges code for token:
   POST /oauth/token
   { "grant_type": "authorization_code", "code": "abc123", ... }

4. Client uses token:
   GET /api/users
   Authorization: Bearer <access_token>

Best practices: Use short-lived access tokens (15 minutes) with refresh tokens. Store tokens securely (httpOnly cookies for web apps). Always use HTTPS.

9. Rate Limiting

Protect your API from abuse and ensure fair usage. Communicate limits via headers.

# Response headers
X-RateLimit-Limit: 1000        # Max requests per window
X-RateLimit-Remaining: 847     # Remaining in current window
X-RateLimit-Reset: 1714003200  # Unix timestamp when window resets

# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after 60 seconds."
  }
}

10. Response Format

Single resource

GET /api/users/123

{
  "data": {
    "id": 123,
    "name": "Alice",
    "email": "[email protected]",
    "createdAt": "2026-01-15T10:30:00Z"
  }
}

Collection

GET /api/users?page=1&limit=2

{
  "data": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "pagination": {
    "page": 1,
    "limit": 2,
    "total": 156
  }
}

Consistent date format

Always use ISO 8601 with timezone: 2026-04-02T14:30:00Z. Never use Unix timestamps in responses (they are unreadable for humans debugging).

11. Discoverability & HATEOAS

Include links to related resources and actions. This makes your API self-documenting.

{
  "data": {
    "id": 123,
    "name": "Alice",
    "links": {
      "self": "/api/users/123",
      "orders": "/api/users/123/orders",
      "avatar": "/api/users/123/avatar"
    }
  }
}

12. Security Checklist

13. Documentation

An undocumented API is an unusable API. At minimum, document every endpoint with:

Use OpenAPI/Swagger for machine-readable specs. Tools like Swagger UI or Redoc generate interactive docs from the spec.

Quick Reference

PrincipleRule
URLsPlural nouns, kebab-case, max 2 levels deep
MethodsGET read, POST create, PUT replace, PATCH update, DELETE remove
Status codes2xx success, 4xx client error, 5xx server error
ErrorsConsistent JSON shape with code, message, details
PaginationAlways paginate lists. Offset or cursor-based.
AuthBearer tokens or API keys. Always HTTPS.
VersioningURL path (/v1/) for breaking changes only
DatesISO 8601 with timezone

Tools for API Development

Format JSON responses, decode JWTs from auth headers, convert timestamps, or generate hashes for API keys — all free in your browser.