← All Articles · · 9 min read

Understanding REST API Design Best Practices

A practical guide to designing REST APIs that developers love — covering resource modeling, HTTP semantics, status codes, versioning, and error handling.

apirestbackendarchitectureweb-development

A REST API is a contract. Every decision you make when designing it — URL structure, status codes, error formats, versioning — becomes a constraint your consumers must work around. Get it right upfront, and integration is smooth. Get it wrong, and you’ll be maintaining backwards-compatibility hacks for years.

Here’s what separates a well-designed REST API from a frustrating one.

Think in Resources, Not Actions

REST is resource-oriented. URLs identify things, HTTP methods express what you’re doing to them.

# Wrong — action-based URLs
POST /createUser
GET  /getUserById?id=42
POST /deleteUser
POST /updateUserEmail

# Right — resource-based URLs
POST   /users          # create a user
GET    /users/42       # get a specific user
DELETE /users/42       # delete a user
PATCH  /users/42       # partially update a user

The HTTP method carries the semantic meaning. Your URL should only identify what resource is being acted on.

Nested Resources

Use nesting for resources that belong to a parent:

GET  /users/42/orders         # all orders for user 42
GET  /users/42/orders/7       # specific order
POST /users/42/orders         # create order for user 42

Limit nesting to one or two levels deep. Beyond that, use query parameters or flat resource paths.

HTTP Methods: Use Them Correctly

Each HTTP method carries a specific meaning that clients and infrastructure depend on.

MethodSemanticsSafeIdempotent
GETReadYesYes
POSTCreateNoNo
PUTFull replaceNoYes
PATCHPartial updateNoNo
DELETEDeleteNoYes

Safe means the request doesn’t modify state — caches and intermediaries can call it freely.

Idempotent means calling it multiple times produces the same result — important for retries on network failure.

# PUT should fully replace the resource
PUT /users/42
{ "name": "Alice Smith", "email": "alice@example.com", "role": "admin" }

# PATCH updates only the fields provided
PATCH /users/42
{ "email": "newalice@example.com" }

Status Codes: Say What You Mean

HTTP status codes are a semantic protocol. Using them correctly means clients can handle responses without parsing error messages.

Success (2xx)

200 OK          — general success (GET, PUT, PATCH)
201 Created     — resource was created (POST), include Location header
204 No Content  — success with no body (DELETE)

Client Errors (4xx)

400 Bad Request      — invalid input, validation failure
401 Unauthorized     — not authenticated (missing/invalid token)
403 Forbidden        — authenticated but not permitted
404 Not Found        — resource doesn't exist
409 Conflict         — state conflict (duplicate key, version mismatch)
422 Unprocessable    — valid format but semantic errors (can't process)
429 Too Many Requests — rate limited

The most common mistake: returning 200 OK with an error in the body. That makes every client parse the response body just to check if the request succeeded.

Consistent Error Responses

Define a standard error format and use it everywhere:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email address is invalid",
    "field": "email",
    "requestId": "req_abc123"
  }
}

Key principles:

  • Always include a machine-readable code (not just the HTTP status)
  • Include a human-readable message for debugging
  • Include a requestId so users can reference it in support tickets
  • For validation errors, include which field failed
// Multiple validation errors
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "requestId": "req_abc123",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "age", "message": "Must be 18 or older" }
    ]
  }
}

Versioning

APIs change over time. Versioning lets you make breaking changes without breaking existing integrations.

URL Versioning (Most Common)

GET /v1/users/42
GET /v2/users/42

Explicit, easy to understand, and works everywhere. The downside: technically violates REST (the version isn’t a resource), but this is the pragmatic choice for most teams.

Header Versioning

GET /users/42
Accept: application/vnd.myapi.v2+json

Cleaner URLs but harder to test in a browser. Use this if you have strong REST purists on the team or if your API is a long-lived platform product.

Rule: Once you publish a versioned API, never make breaking changes to it. Add fields (non-breaking), don’t remove them. Introduce new behavior in /v2, not by silently changing /v1.

Pagination

Never return unbounded lists. Always paginate collections.

Cursor-Based (Preferred)

GET /posts?cursor=abc123&limit=20

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

Cursor pagination is stable — inserting new records doesn’t cause items to skip or duplicate across pages.

Offset-Based

GET /posts?page=3&per_page=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "perPage": 20,
    "total": 147,
    "totalPages": 8
  }
}

Offset pagination is simpler to implement and supports random page access. It breaks under concurrent inserts, but that’s often acceptable for non-real-time data.

Filtering, Sorting, and Searching

Use query parameters for these operations:

GET /users?status=active                    # filter
GET /posts?sort=created_at&order=desc       # sort
GET /users?q=alice                          # search
GET /orders?created_after=2026-01-01        # date range
GET /products?category=tools&min_price=10  # combine filters

Keep query parameter names consistent across all your endpoints. If it’s sort on one endpoint, it shouldn’t be order_by on another.

Security Defaults

A few non-negotiable security practices:

Always use HTTPS. Redirect HTTP to HTTPS; don’t serve APIs over plain HTTP.

Return minimal data. Don’t include internal IDs, passwords, server paths, or internal state in responses. Think about what a consumer needs, not what’s convenient to serialize.

Set CORS correctly. Don’t use Access-Control-Allow-Origin: * for APIs that handle authenticated data.

Rate limit everything. Even internal APIs. Return 429 Too Many Requests with a Retry-After header when limits are hit:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711000000

Documentation as Contract

The best APIs treat documentation as part of the product, not an afterthought. Use OpenAPI/Swagger to define your API schema:

paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User found
        '404':
          description: User not found

An OpenAPI spec generates documentation, client SDKs, and mock servers. It’s the single source of truth for your API contract.

Summary

Good REST API design comes down to consistency and respecting the HTTP protocol:

  • URLs identify resources (nouns), methods express actions (verbs)
  • Use HTTP methods and status codes as intended
  • Define a standard error format and use it everywhere
  • Version from day one — breaking changes need new versions
  • Always paginate collections
  • Document with OpenAPI

The payoff is an API that developers can integrate confidently, that’s easy to maintain, and that grows without accumulating technical debt.

Free Newsletter

Level Up Your Dev Workflow

Get new tools, guides, and productivity tips delivered to your inbox.

Plus: grab the free Developer Productivity Checklist when you subscribe.

Found this guide useful? Check out our free developer tools.