REST API Design for Production Systems
By Oleksandr Andrushchenko — Published on — Modified on
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?
- REST Principles
- Resource Modeling
- HTTP Methods
- URI Design
- Request and Response Design
- Idempotency
- Pagination
- Filtering, Sorting, and Searching
- PATCH vs PUT
- API Versioning
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Conclusion
- More Articles to Read
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.
More Articles to Read
- API Authentication and Authorization Explained
- API Rate Limiting and Throttling Explained
- API Pagination, Filtering, and Sorting Explained
- API Versioning and Backward Compatibility
- API Performance and Reliability Best Practices
Comments (0)