Overview
REST is the dominant architectural style for designing networked APIs. A well-designed REST API uses HTTP semantics consistently, provides predictable URLs, and returns meaningful status codes. Poor API design leads to confused consumers, broken clients, and brittle integrations.
When to Use
Use this resource when:
- Designing a new public or internal API from scratch
- Refactoring a legacy RPC-style API to REST
- Documenting an API with OpenAPI/Swagger
- Choosing between REST, GraphQL, or gRPC for a new service
When to Avoid
- Real-time bidirectional communication: REST is request-response only.
- Complex client-driven queries: GraphQL lets clients request exactly the fields they need. REST over-fetches or under-fetches.
- High-performance internal calls: gRPC with Protobuf is 5-10x faster than REST/JSON for internal microservices.
- Streaming large payloads: REST buffers entire responses.
Solution
Resource Naming
GET /users # List users
GET /users/:id # Get a user
POST /users # Create a user
PUT /users/:id # Full update
PATCH /users/:id # Partial update
DELETE /users/:id # Remove a user
GET /users/:id/orders # Nested resource
Status Codes
// Successful responses
200 OK // GET, PUT, DELETE success
201 Created // POST success
204 No Content // DELETE success (optional)
// Client errors
400 Bad Request // Validation failure
401 Unauthorized // Missing auth token
403 Forbidden // Insufficient permissions
404 Not Found // Resource does not exist
409 Conflict // Duplicate or state conflict
422 Unprocessable // Semantic validation error
// Server errors
500 Internal Error // Unexpected server failure
502 Bad Gateway // Upstream failure
503 Service Unavail // Rate limiting or maintenance
Pagination with Cursor
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ==",
"prev_cursor": null,
"has_more": true
}
}
Error Response Format
Return errors in a consistent structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"field": "email",
"details": [{"field": "email", "message": "Email is required"}]
}
}
Versioning Strategies
// URL-based (most common)
GET /v1/users
GET /v2/users
// Header-based (cleaner URLs, harder to test)
Accept: application/vnd.api+json;version=1
// Query parameter (easy but not recommended)
GET /users?version=1
URL-based versioning is the most explicit and easiest to test. Header-based is cleaner but harder to debug in browsers.
Idempotency Keys
For POST requests that may be retried (payments, order creation), accept an idempotency key:
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{"amount": 1000, "currency": "USD"}
The server stores the key and returns the original response on retry. See Idempotent Endpoints for implementation.
Rate Limiting Response
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1719900000
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after 60 seconds."
}
}
Explanation
REST uses HTTP as an application protocol, not just a transport:
- Idempotency: GET, PUT, DELETE should be safe to retry. See Idempotent Endpoints for patterns. POST is not idempotent.
- Statelessness: Each request contains all information needed; no server-side session.
- Cacheability: Use Cache-Control, ETag, and Last-Modified headers aggressively.
- HATEOAS: Include links to related resources (optional but improves discoverability).
Variants
| Style | Use Case | Notes |
|---|---|---|
| REST | CRUD, resource-oriented | Mature ecosystem; HTTP caching |
| GraphQL | Flexible queries; mobile | Single endpoint; client-driven |
| gRPC | Internal microservices | Binary; streaming; schema-first |
| JSON-RPC | Simple RPC | Lightweight; less HTTP-native |
| tRPC | TypeScript end-to-end | Type-safe; no codegen; TS only |
| SOAP | Enterprise; banking | XML; WS-Security; verbose |
Advanced: Content Negotiation
Support multiple response formats via Accept headers:
GET /users/42
Accept: application/json # default
Accept: application/xml # legacy clients
Accept: application/csv # data export
Server selects the serializer based on Accept. Return 406 Not Acceptable if the format is unsupported.
Advanced: Conditional Requests
Use ETag and If-None-Match for caching:
# First request
GET /users/42
ETag: "abc123"
# Subsequent request
GET /users/42
If-None-Match: "abc123"
# Server returns 304 if unchanged
HTTP/1.1 304 Not Modified
For concurrent updates, use If-Match with ETag for optimistic locking:
PUT /users/42
If-Match: "abc123"
If the ETag no longer matches (someone else modified the resource), return 412 Precondition Failed.
What Works
- Use plural nouns: /orders, not /order or /getOrder
- Version in URL: /v1/users (more explicit than headers)
- Return consistent envelope: { data, error, meta } structure
- Support filtering: GET /users?
- Rate limit early: Return 429 with Retry-After header. See Rate Limiting with Redis for implementation.
Common Mistakes
- Using verbs in URLs: /createUser, /getOrders — use nouns and HTTP methods instead
- Ignoring HTTP status codes: Returning 200 with an error body breaks middleware. See Error Handling for status code usage.
- Not versioning: Breaking changes without versioning strand existing clients
- Over-fetching: Returning huge nested objects when clients need a subset
- Missing content negotiation: Not respecting Accept and Content-Type headers
Troubleshooting
- 5xx errors under load: check rate limits, connection pools, and downstream timeouts.
- CORS errors in the browser: confirm allowed origins, methods, and headers. Preflight requests must return the right headers before the actual request.
- Unexpected 404s: verify route definitions, path parameters, and base paths. Watch for trailing slashes and URL encoding differences.
- Authentication failures: validate token expiry, signature algorithms, and clock skew. Log rejected tokens without exposing secrets.
- Slow response times: profile the slowest percentiles.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the rest-api and http guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply rest api design: what works when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Copying the example without adapting it to real data volumes and failure modes.
- Skipping load and error-injection tests before the first production deployment.
- Hard-coding values that should be configurable per environment.
- Forgetting to add logging and monitoring at each step.
- Deploying without a rollback plan or a tested backup strategy.
- Assuming the minimal example will scale without adding caching or batching.
- Not documenting the version and configuration used in production.
- Letting the recipe sit unchanged when dependencies or scale evolve.
Frequently Asked Questions
Should I use PUT or PATCH for updates?
PUT for full replacement (all fields required). PATCH for partial updates (only changed fields). PUT is idempotent: sending the same PUT twice produces the same state. PATCH can be idempotent but is not required to be.
How do I handle file uploads in REST?
Use multipart/form-data for simple uploads. For large files, use signed URLs (S3, GCS) or resumable uploads. The client uploads directly to object storage, then notifies your API with the file location. This avoids streaming large files through your API server.
Is HATEOAS worth implementing?
For public APIs consumed by diverse clients, yes — it improves discoverability and reduces hardcoding of URLs. For internal APIs with generated clients, optional. Most production APIs skip HATEOAS and document URLs in OpenAPI specs instead.
How do I handle pagination for large datasets?
Use cursor-based pagination for large or frequently changing datasets. Offset-based pagination (page=2&limit=20) is simpler but skips items when data is inserted between requests. Encode the cursor as base64 of the last item's sort key.
What HTTP methods should I use?
GET (read, cacheable), POST (create, not idempotent), PUT (full update, idempotent), PATCH (partial update), DELETE (remove, idempotent). Never use GET for state changes — it breaks caching and violates HTTP semantics.
How do I version my API?
URL-based versioning (/v1/users) is the most common and easiest to test. Bump the version on breaking changes: removed fields, changed types, changed semantics. Non-breaking changes (new fields, new endpoints) do not require a version bump.
Should I wrap responses in an envelope?
For list endpoints, yes — include pagination metadata. For single resources, wrapping is optional. If you wrap, use a consistent structure: { data, error, meta }. Some APIs return data directly with error info in headers.
How do I handle authentication in REST?
Bearer tokens in the Authorization header: Authorization: Bearer <token>. API keys in headers (X-API-Key) for simple cases. Avoid putting tokens in URL parameters — they appear in server logs and browser history.
What is the difference between 401 and 403?
401 Unauthorized means the request lacks authentication credentials. 403 Forbidden means the credentials are valid but the user lacks permission for the specific resource. Always return 401 before auth, 403 after auth but lacking permissions.
How do I handle long-running operations?
Return 202 Accepted with a status URL. The client polls the status URL until the operation completes. For webhooks, return 202 and send a POST to the client's webhook URL when done. See Async API Pattern for patterns.
Related Resources
API Error Response Template
A reusable template for consistent, informative, and developer-friendly API error responses that reduce debugging time.
GuideREST API Design Guide
A thorough guide to designing clean, scalable, and maintainable REST APIs.
RecipeCall a REST API: Python, JavaScript, Java & Go Examples
How to make HTTP requests to a REST API and handle the JSON response in Python, JavaScript, Java, and Go.
RecipeHandle CORS Correctly
How to configure Cross-Origin Resource Sharing (CORS) headers correctly for APIs, SPAs, and serverless functions without opening security holes.
RecipeIdempotent API Endpoints
How to design and implement idempotent API endpoints that safely handle retries, duplicate requests, and network failures without side effects.
RecipegRPC API with Protocol Buffers
Implement a gRPC API with Protocol Buffers. Covers service definition, code generation, client/server examples in Python, Java, and Go.