REST #
REST is an architectural style that has survived for more than two decades and remains the primary choice for building APIs in most modern systems. But “surviving” doesn’t mean everyone understands it correctly — many APIs claim to be RESTful but are actually just “HTTP + JSON” without truly applying the principles that make REST powerful. As a result, APIs that should be predictable and consistent end up full of inconsistencies, wrong status codes, nonsensical URLs, and unexpected breaking changes. This article covers REST from its original philosophy: what constraints define it, why those constraints exist, how to apply them correctly, and when REST isn’t the right choice at all.
REST’s History and Philosophy #
REST was introduced by Roy Fielding in his 2000 dissertation titled Architectural Styles and the Design of Network-based Software Architectures. Fielding was one of the main contributors to the HTTP/1.1 specification — and REST was born from his observation of why the web could scale to a global level never imagined before.
Fielding’s most important insight: the web scaled successfully not just because of its technology, but because of its architecture — how components interact, how state is managed, how resources are identified. REST is the formalization of those architectural principles.
Three important things often misunderstood about REST:
REST isn't a protocol → it's an architectural style
REST isn't a formal standard → it's a set of constraints
HTTP isn't part of REST's definition → HTTP is the most natural implementation
because it's designed to align with REST
The implication: RESTful systems that don’t use HTTP can exist (though rare in practice), and APIs using HTTP can exist that aren’t RESTful at all.
The Six REST Constraints #
Roy Fielding defined REST through six constraints. A system can only be called “RESTful” if it satisfies all the mandatory ones.
flowchart TD
REST["RESTful System"]
CS["1. Client-Server\nSeparation of concerns:\nUI vs Data & Logic"]
SL["2. Stateless\nEvery request self-contained\nNo server-side sessions"]
CA["3. Cacheable\nResponses define their\nown cacheability"]
UI["4. Uniform Interface\nConsistent, uniform\ncontract"]
LS["5. Layered System\nClients don't know\nhow many layers exist"]
COD["6. Code on Demand\n(Optional)\nServer sends executable code"]
REST --> CS
REST --> SL
REST --> CA
REST --> UI
REST --> LS
REST --> COD
style REST fill:#4A90D9,color:#fff,stroke:#2C6FAC
style COD fill:#95A5A6,color:#fff,stroke:#7F8C8D1. Client-Server #
Client and server are clearly separated — the client is responsible for UI and user experience, the server for data and business logic. Both can evolve independently.
Benefits of client-server separation:
✓ Mobile apps, web apps, and third-party consumers can use the same API
✓ The server can scale independently of the client
✓ Frontend and backend teams can work in parallel based on the API contract
✓ Clients can be replaced (e.g. from web to mobile) without changing the server
2. Stateless #
Every request must carry all the information needed to process it. The server doesn’t store state from previous requests — no sessions, no context stored server-side between requests.
// ANTI-PATTERN: Stateful server
POST /api/login → the server stores a session in memory
GET /api/profile → the server looks up the session to know who's requesting
→ Problem: can't scale horizontally (the session lives on one server)
→ Problem: if the server restarts, all sessions are lost
// CORRECT: Stateless with tokens
POST /api/auth/login → the server returns a JWT token
GET /api/profile
Authorization: Bearer ***
→ The token carries all the information the server needs
→ Requests can be handled by any server in the cluster
Stateless is the constraint with the most direct impact on scalability. Stateless systems can scale horizontally easily — add new servers without worrying about state synchronization.
3. Cacheable #
Responses must explicitly define whether they can be cached or not, for how long, and under what conditions the cache must be invalidated. HTTP provides rich caching mechanisms: Cache-Control, ETag, Last-Modified, Expires.
Examples of caching response headers:
GET /api/products/catalog
Cache-Control: public, max-age=3600 → cacheable for 1 hour
ETag: "abc123def456" → content version identifier
GET /api/users/123/profile
Cache-Control: private, max-age=300 → cached client-side only, 5 minutes
Vary: Authorization → cache varies per user
POST /api/orders
Cache-Control: no-store → don't cache at all
With proper caching, many requests never need to reach the server — CDNs, proxies, or browsers can serve them directly. This is what allows large-scale public APIs to serve millions of requests per day.
4. Uniform Interface #
This is the most significant and most often violated constraint. A uniform interface means interacting with all resources follows the same, consistent pattern. Fielding defined it through four sub-constraints:
a. Resource identification through URIs
Every resource has a unique, unchanging identifier:
/users/123, /orders/abc, /products/xyz-001
b. Resource manipulation through representations
Clients send resource representations (JSON/XML) to change its state
Clients don't need to know the server's internal structure
c. Self-descriptive messages
Every request and response carries enough information to be understood:
Content-Type: application/json, proper HTTP status codes, etc.
d. HATEOAS (Hypermedia as the Engine of Application State)
Responses contain links to relevant next operations
(rarely fully applied in practice, but the principle matters)
5. Layered System #
Clients don’t need to know whether they’re connected directly to the origin server or through intermediate layers — load balancers, API gateways, CDNs, cache proxies, or security layers. Each layer only knows the layer it directly interacts with.
flowchart LR
Client["Client"]
CDN["CDN / Edge Cache"]
GW["API Gateway\n(Auth, Rate Limit)"]
LB["Load Balancer"]
S1["Server 1"]
S2["Server 2"]
S3["Server 3"]
Client -->|Request| CDN
CDN -->|Cache miss| GW
GW -->|Verified request| LB
LB --> S1
LB --> S2
LB --> S3
style Client fill:#2C3E50,color:#fff
style CDN fill:#27AE60,color:#fff
style GW fill:#E67E22,color:#fff
style LB fill:#8E44AD,color:#fffLayered systems allow adding infrastructure concerns (security, caching, logging, rate limiting) without changing the API contract.
6. Code on Demand (Optional) #
Servers can send executable code to clients — JavaScript in browsers is the most common example. This is the only optional constraint, and rarely relevant for modern APIs consumed by non-browser clients.
Resource-Oriented Design #
The core of REST is thinking in resources, not actions. A resource is an entity with identity — user, order, product, invoice. Operations on resources are done through HTTP methods, not through verbs in URLs.
URLs as Resource Identifiers #
// ✗ Verbs in URLs (not REST):
GET /api/getUser?id=123
POST /api/createOrder
POST /api/deleteProduct/456
GET /api/fetchOrdersByUser?userId=789
POST /api/updateUserStatus
// ✓ Resource-oriented URLs:
GET /api/users/123
POST /api/orders
DELETE /api/products/456
GET /api/users/789/orders
PATCH /api/users/123/status
HTTP Methods as Operations #
GET → Read a resource (safe, idempotent)
POST → Create a new resource (not idempotent)
PUT → Replace a resource entirely (idempotent)
PATCH → Partially update a resource (idempotent if implemented correctly)
DELETE → Delete a resource (idempotent)
Idempotent = the same result if performed multiple times
GET /users/123 → always returns user 123's data
DELETE /users/123 → first: delete. Second, third: 404. No additional effects.
POST /users → every call creates a new user → NOT idempotent
URL Naming Conventions #
Consistent rules:
✓ Use plural nouns: /users, /orders, /products (not /user, /order)
✓ Use lowercase: /product-categories (not /ProductCategories)
✓ Use kebab-case for multi-word: /order-items (not /order_items or /orderItems)
✓ Nested resources for relations: /users/123/orders/456
✓ Query parameters for filter/sort/pagination: /orders?status=paid&sort=created_at
✗ Don't: verbs in URLs (/createUser, /deleteOrder)
✗ Don't: file formats in URLs (/users.json)
✗ Don't: overly deep nesting: /users/123/orders/456/items/789/discounts
→ better: /order-items/789 or /orders/456/items/789
Correct HTTP Status Codes #
Using proper HTTP status codes is part of the uniform interface — every response carries enough information to be understood without reading the body.
2xx — Success
200 OK → Request succeeded, data returned
201 Created → New resource successfully created
202 Accepted → Request accepted, processed async (not yet complete)
204 No Content → Succeeded, no data returned (usually DELETE)
3xx — Redirect
301 Moved Permanently → Resource permanently moved (update bookmarks)
304 Not Modified → Cache still valid (matches the existing ETag)
4xx — Client Errors
400 Bad Request → Malformed request or invalid parameters
401 Unauthorized → Not authenticated (token missing or invalid)
403 Forbidden → Authenticated but no access
404 Not Found → Resource not found
405 Method Not Allowed→ HTTP method not supported for this resource
409 Conflict → State conflict (duplicate, version mismatch)
422 Unprocessable → Business validation failed (format-valid input, logic failed)
429 Too Many Requests → Rate limit exceeded
5xx — Server Errors
500 Internal Server Error → Unexpected server error
502 Bad Gateway → Upstream server error
503 Service Unavailable → Server unavailable (maintenance/overload)
504 Gateway Timeout → Upstream server timeout
// ANTI-PATTERN: Always returning 200
HTTP 200 OK
{
"success": false,
"error": "User not found",
"code": 404
}
→ Clients must always parse the body to know success/failure
→ Monitoring can't detect errors from status codes
→ Can't leverage HTTP infrastructure (retry logic, circuit breakers)
// CORRECT: Semantically correct status codes
HTTP 404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with id 123 not found"
}
}
The difference between 401 and 403 is often confused. 401 Unauthorized means “I don’t know who you are” — authentication is required or the token is invalid. 403 Forbidden means “I know who you are, but you’re not allowed to do this” — authentication succeeded but authorization failed. Using 403 when a token is missing (should be 401) is a common mistake.
Consistent Response Formats #
Response format consistency is often underestimated but greatly affects the developer experience of an API’s consumers.
Success response format (200, 201):
// Single resource
GET /api/users/123
{
"data": {
"id": "usr_123",
"name": "Budi Santoso",
"email": "[email protected]",
"created_at": "2024-01-15T10:00:00Z"
}
}
// Collection with pagination
GET /api/users?page=2&limit=20
{
"data": [...],
"meta": {
"page": 2,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
Error response format (4xx, 5xx):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "phone", "message": "Phone number is required" }
]
}
}
Three rules to keep consistent:
datafor success payloadsmetafor pagination and metadataerrorfor all error conditions — withcode(machine-readable) andmessage(human-readable)
Pagination, Filtering, and Sorting #
APIs returning all data at once are APIs that will become problems as data grows. Pagination, filtering, and sorting must be designed from the start.
Pagination #
// Offset-based pagination (most common):
GET /api/orders?page=3&per_page=20
Response:
{
"data": [...],
"meta": {
"page": 3,
"per_page": 20,
"total": 547,
"total_pages": 28,
"has_next": true,
"has_prev": true
}
}
// Cursor-based pagination (better for real-time data):
GET /api/orders?cursor=***&limit=20
Response:
{
"data": [...],
"meta": {
"next_cursor": "***",
"prev_cursor": "***",
"has_next": true
}
}
Cursor-based is better for rapidly changing data — offset-based can “skip” or “duplicate” data if inserts/deletes happen between pages.
Filtering and Sorting #
// Filtering:
GET /api/orders?status=paid&user_id=123
GET /api/products?category=electronics&min_price=100&max_price=500
// Sorting (minus prefix for descending):
GET /api/orders?sort=-created_at → newest first
GET /api/orders?sort=total,-created_at → sort by total ASC, then created_at DESC
// Field selection (reduces over-fetching):
GET /api/users/123?fields=id,name,email
→ Only returns the requested fields
Versioning Strategies #
Every API used by others will need to change. Versioning is how to make breaking changes without breaking existing consumers.
Three versioning approaches:
1. URL path versioning (most common and clearest):
/api/v1/users
/api/v2/users
✓ Clearly visible in the URL
✓ Easy to test in a browser
✗ "Clutters" the URL
2. Header versioning:
GET /api/users
Accept: application/vnd.myapi.v2+json
✓ URLs stay clean
✗ Not visible in the URL, harder to debug
3. Query parameter versioning:
GET /api/users?version=2
✓ Easy for testing
✗ Can be cached without the correct version
Recommendations:
→ Use URL path versioning for the majority of use cases
→ Start with /api/v1 from day one, even with no v2 plans
→ Keep old versions alive for at least 6-12 months after v2 releases
→ Communicate the deprecation schedule well in advance
// ANTI-PATTERN: Breaking changes without versioning
Sprint 5: GET /api/users/123 → { "user_name": "Budi" }
Sprint 8: GET /api/users/123 → { "name": "Budi" } ← field name changed
→ All consumers using "user_name" break immediately
// CORRECT: Versioning for breaking changes
/api/v1/users/123 → { "user_name": "Budi" } ← still working
/api/v2/users/123 → { "name": "Budi" } ← changes in the new version
Deprecation notice in v1 response headers:
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Handling Non-CRUD Operations #
REST is most natural for CRUD operations (Create, Read, Update, Delete). But not all operations map directly to CRUD. Several approaches for more complex operations:
Approach 1: Model actions as resources (most RESTful)
POST /api/orders/123/cancellation → creates a "cancellation" resource
POST /api/users/123/password-reset → creates a "password reset" resource
POST /api/payments/123/refund → creates a "refund" resource
Approach 2: Use sub-resources with clear verbs
POST /api/orders/123/cancel → explicit action on a resource
POST /api/emails/123/send → send an email
Approach 3: For batch or bulk operations
POST /api/users/batch → create many users at once
Body: { "users": [...] }
PATCH /api/orders → update many orders at once
Body: { "ids": [1,2,3], "status": "shipped" }
What should be avoided:
✗ POST /api/doSomething → verb in the URL
✗ GET /api/runJob → side effects in GET
Request and Response Best Practices #
Important Request Headers #
Content-Type: application/json → format of the sent body
Accept: application/json → desired response format
Authorization: Bearer *** → authentication
X-Request-ID: uuid-v4 → tracing (optional but very useful)
X-Idempotency-Key: unique-key → for operations needing idempotency
Important Response Headers #
Content-Type: application/json; charset=utf-8
X-Request-ID: <echo of request or newly generated>
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1706352000
Cache-Control: private, max-age=300
Idempotency for Important Operations #
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: Scenario: a POST request fails midway
C->>S: POST /api/orders\nX-Idempotency-Key: key-abc123\nBody: { items: [...] }
S-->>C: ❌ Network timeout — the client doesn't know if the order was created
Note over C,S: Client retries with the same key
C->>S: POST /api/orders\nX-Idempotency-Key: key-abc123\nBody: { items: [...] }
S-->>C: 200 OK (not 201) — the same order is returned\nwithout creating a duplicateREST vs SOAP vs GraphQL vs gRPC #
SOAP:
Format: XML only, heavy
Complexity: High (WSDL, WS-Security, etc.)
Use case: Enterprise legacy, banking, insurance needing formal contracts
When chosen: When forced to integrate with legacy systems
REST:
Format: JSON (usually), flexible
Complexity: Low
Use case: Public APIs, web/mobile backends, the majority of use cases
When chosen: The default choice for almost all new APIs
GraphQL:
Format: JSON
Complexity: Medium (more complex server, more flexible client)
Use case: Frontend-heavy with many query variations
When chosen: A real need for flexible queries from multi-clients
gRPC:
Format: Binary (Protobuf)
Complexity: High
Use case: Internal service-to-service, high throughput
When chosen: Performance is the main constraint and all services are internal
REST Anti-Patterns to Avoid #
// ✗ Anti-pattern 1: Verbs in URLs
POST /api/createUser
GET /api/getUserById?id=123
POST /api/deleteOrder/456
// ✓ Solution: Nouns in URLs, HTTP methods as verbs
POST /api/users
GET /api/users/123
DELETE /api/orders/456
---
// ✗ Anti-pattern 2: Always returning 200
HTTP 200 OK
{ "error": true, "message": "Not found" }
// ✓ Solution: Semantically correct status codes
HTTP 404 Not Found
{ "error": { "code": "NOT_FOUND", "message": "..." } }
---
// ✗ Anti-pattern 3: Inconsistent response formats
GET /users/1 → { "userId": 1, "userName": "Budi" }
GET /orders/1 → { "id": 1, "order_user": "Budi" }
GET /products/1 → { "productId": 1, "product_name": "..." }
// ✓ Solution: One convention for all resources
All use: { "id": ..., snake_case for all fields }
---
// ✗ Anti-pattern 4: Breaking changes without versioning
The "email" field removed from responses directly
→ Consumers using "email" break immediately
// ✓ Solution: Versioning + deprecation periods
Add in v2, keep v1 with a deprecation notice
---
// ✗ Anti-pattern 5: Overly granular or overly generic endpoints
Too granular:
GET /api/user-first-name/123
GET /api/user-last-name/123
GET /api/user-email/123
→ Many requests needed for one user's data
Too generic:
POST /api/action
Body: { "type": "create_user", "data": {...} }
→ Eliminates all REST advantages
// ✓ Solution: Resources at the right granularity
GET /api/users/123
→ One request, complete user data
When Not to Use REST #
REST isn’t the solution to every problem. There are situations where other approaches are more appropriate:
Use WebSocket if:
✓ Real-time bidirectional communication is needed
✓ Chat, collaborative editing, live dashboards, gaming
→ REST request-response is inefficient for these
Use SSE (Server-Sent Events) if:
✓ Server push to clients is needed (one-way)
✓ Live notifications, progress tracking
→ REST polling is too expensive
Use gRPC if:
✓ Internal service-to-service communication
✓ High throughput, performance is the main constraint
✓ Data streaming
→ REST overhead is too high for these
Use GraphQL if:
✓ Frontends have very diverse data needs
✓ Multiple different clients (web, mobile, widgets) need different data
✓ Over-fetching is already a real, measurable problem
→ REST over-fetching is significantly harmful
Keep using REST if:
✓ Public APIs consumed by external parties
✓ Web or mobile backends with standard use cases
✓ The team has no specific problem requiring another solution
→ The safe, familiar default
Healthy REST API Checklist #
URL AND RESOURCE DESIGN:
□ URLs use nouns, not verbs
□ Resource names plural and lowercase
□ Multi-word names use kebab-case
□ Resource nesting at most 2-3 levels
□ Query parameters for filtering, sorting, pagination
HTTP METHODS AND STATUS CODES:
□ GET only for reads, no side effects
□ POST for create, PUT/PATCH for update, DELETE for delete
□ Status codes semantically correct (not always 200)
□ 401 vs 403 correctly distinguished
□ 422 for business validation, 400 for format/parameter errors
RESPONSE FORMATS:
□ Consistent format across all endpoints (data, meta, error)
□ Error responses have code (machine-readable) and message (human-readable)
□ Pagination info in meta for collection endpoints
□ Timestamps in ISO 8601 (not Unix timestamps unless there's a reason)
□ IDs don't leak internal details (use UUIDs or prefixed IDs)
VERSIONING AND COMPATIBILITY:
□ Versioning exists from day one (/v1/)
□ Breaking changes only through new versions
□ Deprecation communicated through headers
□ Old versions kept with a sufficient grace period
SECURITY:
□ Authentication on all endpoints that need it (401 for unauthorized)
□ Authorization at the resource level (403 for forbidden)
□ Input validated before processing
□ Rate limiting applied
□ HTTPS only in production
DOCUMENTATION:
□ OpenAPI/Swagger spec available and synced with implementation
□ All endpoints documented with request and response examples
□ Error codes and their meanings documented
□ Authentication flows documented
Summary #
- REST is an architectural style, not a protocol — not just “HTTP + JSON”. Understanding its six constraints (client-server, stateless, cacheable, uniform interface, layered system, code on demand) is the key to building truly RESTful APIs.
- Stateless is the foundation of scalability — the server not storing client state enables horizontal scaling without cross-server coordination. Token-based auth (JWT) is the correct implementation; server-side sessions violate this constraint.
- URLs are resource identifiers, HTTP methods are operations — use nouns in URLs (/users/123), HTTP methods as verbs (GET, POST, PUT, PATCH, DELETE). Verbs in URLs are the most commonly found anti-pattern.
- HTTP status codes are part of the contract — always return semantically correct status codes. 401 for not authenticated, 403 for no access, 422 for failed business validation. Returning 200 for every response destroys the value of the uniform interface.
- Response format consistency matters more than structural perfection — pick one convention (data/meta/error) and apply it consistently across all endpoints. Inconsistency forces consumers to learn each endpoint individually.
- Versioning must exist from day one — start with /api/v1 even without change plans. Breaking changes without versioning are contract violations that break consumers.
- Pagination, filtering, and sorting must be designed from the start — APIs returning unlimited data are APIs that become problems as data grows.
- Caching is REST’s often-untapped advantage — use Cache-Control, ETag, and conditional requests to significantly reduce server load, especially for rarely changing data.
- REST isn’t for every use case — real-time needs WebSocket/SSE, high-throughput service-to-service needs gRPC, flexible multi-client queries need GraphQL. Choose REST because it’s right, not because it’s familiar.
- Documentation is part of the API, not an add-on — an OpenAPI spec synced with implementation is the written contract letting consumers and providers evolve independently.
#
- REST is an architectural style, not a protocol — not just “HTTP + JSON”. Understanding its six constraints (client-server, stateless, cacheable, uniform interface, layered system, code on demand) is the key to building truly RESTful APIs.
- Stateless is the foundation of scalability — the server not storing client state enables horizontal scaling without cross-server coordination. Token-based auth (JWT) is the correct implementation; server-side sessions violate this constraint.
- URLs are resource identifiers, HTTP methods are operations — use nouns in URLs (/users/123), HTTP methods as verbs (GET, POST, PUT, PATCH, DELETE). Verbs in URLs are the most commonly found anti-pattern.
- HTTP status codes are part of the contract — always return semantically correct status codes. 401 for not authenticated, 403 for no access, 422 for failed business validation. Returning 200 for every response destroys the value of the uniform interface.
- Response format consistency matters more than structural perfection — pick one convention (data/meta/error) and apply it consistently across all endpoints. Inconsistency forces consumers to learn each endpoint individually.
- Versioning must exist from day one — start with /api/v1 even without change plans. Breaking changes without versioning are contract violations that break consumers.
- Pagination, filtering, and sorting must be designed from the start — APIs returning unlimited data are APIs that become problems as data grows.
- Caching is REST’s often-untapped advantage — use Cache-Control, ETag, and conditional requests to significantly reduce server load, especially for rarely changing data.
- REST isn’t for every use case — real-time needs WebSocket/SSE, high-throughput service-to-service needs gRPC, flexible multi-client queries need GraphQL. Choose REST because it’s right, not because it’s familiar.
- Documentation is part of the API, not an add-on — an OpenAPI spec synced with implementation is the written contract letting consumers and providers evolve independently.