REST API Design for Production Systems

By Oleksandr Andrushchenko — Published on — Modified on

What is a REST API?
What is a REST API?

REST remains the most common architectural style for public APIs, mobile backends, SaaS platforms, and microservices. While building a REST API is relatively simple, designing one that remains intuitive, backward compatible, scalable, and maintainable for years requires much more than choosing HTTP methods correctly.

Good REST APIs are built around stable resource models, predictable behavior, consistent error handling, and contracts that clients can safely depend on. The goal is not simply exposing data—it is creating an interface that allows independent systems to evolve without breaking one another.

Table of Contents

What Makes a Good REST API?

Many APIs work correctly but become difficult to maintain after several years because every endpoint follows different naming conventions, returns different error formats, or models similar resources differently.

A production-quality REST API should feel predictable. After understanding one endpoint, developers should already have a good idea how every other endpoint behaves.

Characteristic Good REST API
Naming Consistent across every resource.
Errors Same structure everywhere.
Authentication Uniform across endpoints.
Filtering Works consistently.
Pagination Uses one approach everywhere.
Status Codes Follow HTTP semantics.

Key Point: consistency is usually more valuable than clever endpoint design.

REST Principles

REST is not a protocol. It is an architectural style that uses standard HTTP concepts to expose resources through a uniform interface.

Production REST APIs generally follow several principles regardless of programming language or framework.

Resources

Everything exposed through the API should represent a resource instead of an implementation detail.

Good

/users
/orders
/products
/payments

Avoid

/getUsers
/updateOrder
/deleteProduct

Resources describe business entities. HTTP methods describe the action performed on them.

Statelessness

Each request should contain everything necessary to process it.

Servers should not rely on previous requests or hidden session state.

Good Bad
JWT bearer token Hidden server session
Explicit request data Depends on previous request
Independent requests Sequential workflow required

Uniform Interface

Clients should interact with every resource using the same HTTP conventions.

GET     /users/123
PATCH   /users/123
DELETE  /users/123

GET     /orders/456
PATCH   /orders/456
DELETE  /orders/456

Rule of Thumb: once developers understand one resource, every other resource should feel familiar.

Resource Modeling

Resource modeling is often the most important API design decision. Poor resource boundaries lead to awkward endpoints, duplicated logic, and difficult evolution.

Resources should represent business concepts rather than database tables.

Resource Identifiers

Every resource should have a stable identifier.

Preferred Avoid
ord_8L4f9P Database auto increment IDs exposed publicly
UUID Composite internal keys
ULID Temporary identifiers

Public identifiers should remain stable even if internal storage changes.

Resource Relationships

Relationships should be represented naturally without deeply nested URLs.

Good

/orders/{id}
/orders/{id}/items

Avoid

/users/{id}/orders/{id}/items/{id}/details/{id}

Deep nesting usually indicates resource boundaries that should be reconsidered.

HTTP Methods

HTTP methods already communicate intent. REST APIs should use them consistently instead of inventing custom behavior.

Method Purpose
GET Read data.
POST Create resource or perform action.
PUT Replace entire resource.
PATCH Partial update.
DELETE Remove resource.

Safe vs Idempotent Operations

Understanding idempotency becomes important once retries, load balancers, proxies, and distributed systems enter production.

Method Safe Idempotent
GET Yes Yes
PUT No Yes
PATCH No Usually
DELETE No Yes
POST No No

Production Note: retries are much easier to implement for idempotent operations.

URI Design

URIs should describe resources instead of operations.

A clean URI hierarchy improves readability and reduces future redesign work.

Collections and Items

GET    /orders

POST   /orders

GET    /orders/{order_id}

PATCH  /orders/{order_id}

DELETE /orders/{order_id}

Collections contain resources. Individual resources are identified by stable IDs.

Actions

Some business operations do not fit CRUD semantics.

Using action endpoints is acceptable when the operation represents a business action rather than resource modification.

POST /orders/{id}/cancel

POST /payments/{id}/capture

POST /users/{id}/verify-email

Trying to force every business operation into CRUD often produces less intuitive APIs.

Request and Response Design

Requests and responses form the long-term API contract. Small inconsistencies accumulate quickly across dozens of endpoints.

Consistency is more valuable than clever serialization techniques.

JSON Structure

Responses should follow predictable field naming and object layouts.

{
  "id": "ord_123",
  "status": "paid",
  "total_cents": 1299,
  "currency": "USD",
  "created_at": "2026-07-10T18:42:00Z"
}

Rule of Thumb: prefer stable, descriptive field names over abbreviated ones.

HTTP Status Codes

Status codes should communicate the result category before the client even parses the response body.

Status Meaning
200 Request succeeded.
201 Resource created.
204 No response body.
400 Validation error.
401 Authentication required.
403 Permission denied.
404 Resource not found.
409 Business conflict.
422 Semantic validation failed.
429 Rate limited.
500 Unexpected server error.

Consistent Error Responses

Every endpoint should return the same error format.

{
  "error": {
    "code": "order_not_found",
    "message": "Order was not found.",
    "request_id": "req_a34f19"
  }
}

Clients should make decisions based on error.code, not by parsing human-readable messages.

Important: request identifiers are invaluable when debugging production incidents across API gateways, logs, distributed services, and customer support tickets.

Idempotency

Idempotency is one of the most important properties of production APIs. An idempotent operation produces the same result regardless of how many times the same request is repeated.

Retries happen because of network failures, client timeouts, proxy failures, or temporary service outages. An API should behave predictably when duplicate requests occur.

Method Normally Idempotent? Notes
GET Yes Reading should not modify state.
PUT Yes Replacing the same resource twice produces the same result.
PATCH Usually Depends on implementation.
DELETE Yes Deleting an already deleted resource should still succeed safely.
POST No Usually creates a new resource.

Production Note: payment APIs commonly support an Idempotency-Key header so accidental retries do not charge customers multiple times.

POST /payments

Idempotency-Key: 4a2f4e59-a9d8-46bc

Request repeated

→ Same payment returned
→ No duplicate charge created

Pagination

Collection endpoints should never return unbounded datasets. Pagination protects databases, reduces response size, improves latency, and prevents excessive memory usage.

The two most common approaches are offset pagination and cursor pagination.

Offset Pagination

Offset pagination is easy to understand and implement.

GET /orders?offset=100&limit=20
Advantage Disadvantage
Simple Slow on very large datasets.
Easy page navigation Results may shift during inserts.

Cursor Pagination

Cursor pagination uses a stable ordering and continues from the last returned record.

GET /orders?after=ord_A7M91D&limit=20

Cursor pagination scales significantly better for large production systems and avoids duplicate or skipped records while new data is being inserted.

Use Case Preferred Strategy
Admin dashboard Offset
Public API Cursor
Infinite scrolling Cursor
Millions of rows Cursor

Filtering, Sorting, and Searching

REST APIs should expose consistent filtering patterns instead of creating custom query parameters for every endpoint.

GET /orders?status=paid

GET /orders?customer_id=cus_123

GET /orders?created_after=2026-01-01

GET /orders?sort=-created_at

GET /orders?search=laptop

Rule of Thumb: every collection endpoint should expose filtering, sorting, and pagination in a consistent way.

Capability Example
Filter ?status=paid
Sort ?sort=-created_at
Search ?search=apple
Limit ?limit=50

PATCH vs PUT

Although both modify resources, they communicate different intent.

Method Meaning
PUT Replace the complete resource.
PATCH Modify selected fields.
{
  "status": "shipped"
}

PATCH usually produces smaller payloads and avoids accidentally overwriting unrelated fields.

API Versioning

Every successful API eventually changes. Versioning allows new functionality without breaking existing clients.

Strategy Example
URI /v1/orders
Header Accept-Version: 2
Media Type application/vnd.company.v2+json

URI versioning is the simplest approach and remains the most common choice for public REST APIs.

Production Note: adding optional fields is usually backward compatible. Removing or changing existing fields rarely is.

Ready-to-Use Example

The following examples illustrate a production-oriented REST API using OpenAPI, FastAPI, and a Python client with sensible defaults.

OpenAPI Specification

openapi: 3.0.3

paths:

  /v1/orders:
    get:
      summary: List orders

      parameters:

        # Cursor used for pagination.
        - name: after
          in: query
          schema:
            type: string

        # Maximum number of returned records.
        - name: limit
          in: query
          schema:
            type: integer
            default: 50

        # Filter only paid orders.
        - name: status
          in: query
          schema:
            type: string

      responses:
        "200":
          description: Successful response.

FastAPI Example

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/v1/orders")
async def list_orders(
    after: str | None = None,

    # Prevent extremely expensive requests.
    limit: int = Query(default=50, le=100),

    status: str | None = None,
):

    # Production code would query a database using
    # cursor pagination instead of OFFSET.
    return {
        "items": [],
        "next_cursor": None
    }

Python Client

import requests

response = requests.get(
    "https://api.example.com/v1/orders",

    headers={
        "Authorization": "Bearer TOKEN"
    },

    params={
        "limit": 50,
        "status": "paid"
    },

    # Never leave HTTP requests without timeouts.
    timeout=3,
)

response.raise_for_status()

print(response.json())

Common Mistakes

  • Using verbs in URLs instead of resources.
  • Returning different error formats from different endpoints.
  • Ignoring pagination on collection endpoints.
  • Returning 200 for every request, even when errors occur.
  • Using mutable image or resource identifiers.
  • Breaking clients by changing response fields.
  • Ignoring idempotency for retryable operations.
  • Not validating request input.
  • Returning internal exception messages to clients.
  • Missing request identifiers in logs and responses.

Production Checklist

  • Model APIs around resources.
  • Keep URI naming consistent.
  • Use HTTP methods correctly.
  • Return meaningful HTTP status codes.
  • Keep error format consistent.
  • Support pagination on collections.
  • Provide filtering and sorting.
  • Support idempotent retries where appropriate.
  • Version APIs before breaking changes.
  • Use request IDs for tracing.
  • Document the API using OpenAPI.
  • Always configure client timeouts.

Conclusion

Good REST API design is about much more than choosing the correct HTTP method. Stable resource modeling, consistent contracts, predictable behavior, proper pagination, safe retries, and thoughtful versioning determine whether an API remains easy to consume years after its initial release.

REST succeeds because it embraces familiar HTTP semantics while keeping systems loosely coupled. Well-designed APIs allow implementations to evolve independently without forcing client applications to change.

Key Takeaway: treat every REST endpoint as a long-term contract. Consistency, simplicity, and backward compatibility almost always provide more value than clever endpoint designs or framework-specific conventions.

Comments (0)