DEVESSENTIALS

REST API Design Best Practices: A Practical Guide

A well-designed REST API is intuitive, consistent, and doesn't surprise its consumers. This guide covers the conventions that separate APIs developers love from APIs that require a support ticket to understand.

Resource Naming and URL Structure

Resources are nouns, never verbs. The HTTP method expresses the action:

# Good
GET    /users          → list users
GET    /users/42       → get user 42
POST   /users          → create user
PUT    /users/42       → replace user 42
PATCH  /users/42       → update user 42 partially
DELETE /users/42       → delete user 42

# Bad — verbs in URLs
GET  /getUser?id=42
POST /createUser
POST /deleteUser/42

Use lowercase, hyphen-separated words for multi-word resources: /order-items, not /orderItems or /order_items. URL paths are case-sensitive on most servers; lowercase avoids confusion.

Nested Resources

Model relationships with nesting, but limit to two levels deep:

GET /users/42/orders        → orders belonging to user 42
GET /users/42/orders/7      → order 7 belonging to user 42

# Avoid deeper nesting — it becomes unwieldy:
GET /users/42/orders/7/items/3/reviews   # too deep

For deeply nested resources, consider flattening: GET /order-items/3 with the parent IDs as query parameters or in the response body.

HTTP Methods

MethodActionIdempotentBody
GETReadYesNo
POSTCreateNoYes
PUTReplaceYesYes
PATCHPartial updateNo*Yes
DELETEDeleteYesNo

*PATCH can be made idempotent with careful design (JSON Patch operations), but isn't by default.

HTTP Status Codes

Use the right status code. Here are the most important ones for REST APIs:

2xx Success

  • 200 OK — general success (GET, PATCH, PUT)
  • 201 Created — resource created (POST). Include a Location header pointing to the new resource.
  • 204 No Content — success with no response body (DELETE, PATCH with no return value)

4xx Client Errors

  • 400 Bad Request — malformed request, validation error
  • 401 Unauthorized — not authenticated (missing or invalid token)
  • 403 Forbidden — authenticated but not authorized
  • 404 Not Found — resource doesn't exist
  • 409 Conflict — state conflict (duplicate, version mismatch)
  • 422 Unprocessable Entity — valid JSON but failed business validation
  • 429 Too Many Requests — rate limit exceeded

5xx Server Errors

  • 500 Internal Server Error — unexpected server failure
  • 503 Service Unavailable — temporarily down, use with Retry-After

See the full reference at HTTP Status Codes.

Error Response Format

Consistent error bodies make client error handling predictable. A widely adopted format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "must be a valid email address"
      },
      {
        "field": "age",
        "message": "must be a positive integer"
      }
    ]
  }
}

Always include a machine-readable code (for programmatic handling) and a human-readable message (for debugging). The details array helps clients surface field-level validation errors to users.

Versioning

Version your API from day one. Breaking changes are inevitable. The three main approaches:

# URL versioning (most common, most explicit)
GET /v1/users
GET /v2/users

# Header versioning
GET /users
Accept: application/vnd.myapi+json;version=2

# Query parameter (least recommended)
GET /users?version=2

URL versioning is the most widely used because it's visible in logs, bookmarkable, and easy to test in a browser. Maintain old versions for at least 6–12 months after announcing deprecation.

Pagination

Never return unbounded lists. Two common pagination patterns:

Offset pagination:

GET /users?page=2&per_page=25

{
  "data": [...],
  "pagination": {
    "page": 2,
    "per_page": 25,
    "total": 143,
    "total_pages": 6
  }
}

Cursor pagination (better for large datasets and real-time data):

GET /users?after=cursor_abc123&limit=25

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

Cursor pagination is more stable — offset pagination breaks when records are inserted or deleted between pages.

Filtering, Sorting, and Field Selection

# Filtering
GET /users?status=active&role=admin

# Sorting
GET /users?sort=created_at&order=desc

# Field selection (reduces payload size)
GET /users?fields=id,name,email

# Search
GET /users?q=alice

Response Envelope

Use a consistent response envelope so clients can reliably parse responses:

// Collection
{
  "data": [ { "id": 1, ... }, { "id": 2, ... } ],
  "pagination": { ... },
  "meta": { "total": 42 }
}

// Single resource
{
  "data": { "id": 1, "name": "Alice", ... }
}

Building or debugging a REST API? Format your JSON responses · HTTP Status Codes reference · Decode JWT tokens

Frequently Asked Questions

Should I use plural or singular nouns for resource names?

Use plural nouns. /users is the collection of all users; /users/42 is one user. Consistent plurals avoid the ambiguity of mixing /user (singular) and /users/42 (plural). Almost every major API (GitHub, Stripe, Twilio) uses plural resource names.

When should I use PUT vs PATCH?

PUT replaces the entire resource with the request body. If you PUT a user with only a name field, all other fields should be cleared or reset to defaults. PATCH applies a partial update — only the fields in the request body are changed. For most update operations, PATCH is more practical and less error-prone. PUT is appropriate when the client is responsible for the complete state of a resource (idempotent creation or replacement).

Is it OK to return 200 for errors?

No. Always use the appropriate HTTP status code. Some legacy APIs return 200 OK with an 'error' field in the body — this breaks HTTP clients, caches, monitoring tools, and any middleware that checks status codes. The HTTP status code is the primary error signal; use it correctly. Your API clients will thank you.

How should I handle API versioning?

URL versioning (/v1/users) is the most explicit and widely understood approach — it's immediately visible in logs, browser address bars, and documentation. Header versioning (Accept: application/vnd.api+json;version=1) is cleaner in theory but harder to test and cache. Query parameter versioning (?version=1) is the least recommended. Whatever you choose, be consistent and document your deprecation policy clearly.