APIs are contracts. Once published, they’re hard to change without breaking clients. Poor API design creates frustrated developers, increased support burden, and technical debt that compounds over time.
Good API design does the opposite. Intuitive APIs attract users. Consistent patterns reduce bugs. Clear documentation accelerates adoption.
This guide covers REST API best practices: resource naming, HTTP methods, versioning, error handling, pagination, and authentication patterns.
Core REST Principles
What REST Actually Means
REST stands for REpresentational State Transfer. Roy Fielding defined it in his 2000 doctoral dissertation as an architectural style with six constraints:1
- Client-Server: Separation of concerns between UI and data storage.
- Stateless: No session state stored on the server between requests.
- Cacheable: Responses must define themselves as cacheable or not.
- Uniform Interface: Consistent structure across the API.
- Layered System: Client can’t tell if connected directly to server.
- Code on Demand (optional): Server can extend client functionality.
Resources, Not Actions
REST is about resources (nouns), not actions (verbs). HTTP methods provide the verbs.
| Wrong | Right |
|---|---|
/getUsers | GET /users |
/createUser | POST /users |
/deleteUser/123 | DELETE /users/123 |
/updateOrder | PATCH /orders/123 |
The URL identifies the resource. The HTTP method specifies the action.
URL Structure and Naming
Use Nouns, Not Verbs
The HTTP method already provides the verb. Don’t repeat it in the URL.
| Bad | Good |
|---|---|
GET /getUsers | GET /users |
POST /createUser | POST /users |
DELETE /deleteUser/123 | DELETE /users/123 |
PUT /updateOrder | PUT /orders/123 |
Use Plural Nouns
Consistency matters more than the specific choice. Plurals are the convention.
/users (collection)
/users/123 (specific user)
/users/123/orders (user's orders)
Using both /user and /users in the same API creates confusion.
Use Lowercase and Hyphens
Good: /user-profiles
Bad: /UserProfiles
Bad: /user_profiles
URLs are case-sensitive on some servers. Lowercase avoids ambiguity. Hyphens are more readable than underscores.
Hierarchy Represents Relationships
/users/123/orders/456/items/789
This URL structure communicates: Item 789 belongs to Order 456, which belongs to User 123.
Avoid Deep Nesting
More than three levels becomes unwieldy.
Too deep: /users/123/projects/456/tasks/789/comments/101
Better alternatives:
/task-comments/101
/tasks/789/comments/101
/comments/101?task_id=789
Query Parameters for Filtering
Use query parameters for filtering, sorting, and pagination—not path segments.
/users?status=active
/orders?created_after=2024-01-01
/products?category=electronics&sort=price
HTTP Methods (CRUD Mapping)
Standard Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Read resource(s) | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource | Yes | No |
| PATCH | Update resource | Yes | No |
| DELETE | Delete resource | Yes | No |
Idempotent: Multiple identical requests produce the same result. Safe: Request doesn’t modify server state.
GET - Read
Retrieve resources. Never modify state with GET requests.
GET /users -> List all users
GET /users/123 -> Get user 123
GET /users?role=admin -> Filtered list
POST - Create
Create new resources. The server assigns the ID.
POST /users
Body: { "name": "John", "email": "john@example.com" }
Response: 201 Created
Location: /users/124
Body: { "id": 124, "name": "John", "email": "john@example.com" }
PUT - Full Replace
Replace the entire resource. Client sends complete representation.
PUT /users/123
Body: { "name": "John", "email": "new@example.com" }
If a field is omitted, it’s removed (or set to default). PUT is idempotent—calling it multiple times produces the same result.
PATCH - Partial Update
Update only specified fields. Other fields remain unchanged.
PATCH /users/123
Body: { "email": "new@example.com" }
Only the email changes. Name and other fields stay the same.
DELETE - Remove
Delete a resource.
DELETE /users/123
Response: 204 No Content
DELETE is idempotent. Deleting an already-deleted resource returns 404, but the end state is the same.
HTTP Status Codes
Use appropriate status codes. They communicate success, failure, and failure reasons.
Success Codes (2xx)
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE (no body returned) |
Client Error Codes (4xx)
| Code | Meaning | Use Case |
|---|---|---|
| 400 | Bad Request | Malformed request syntax |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | State conflict (duplicate, version mismatch) |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
Server Error Codes (5xx)
| Code | Meaning | Use Case |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Maintenance or overload |
Choosing the Right Code
Be specific. Use 422 for validation errors, 400 for malformed requests. Use 401 for “who are you?” and 403 for “I know who you are, but you can’t do this.”
Never use 200 for errors. The status code should reflect the actual result.
Versioning Strategies
APIs evolve. Breaking changes happen. Versioning lets you evolve while giving clients time to migrate.
Why Version?
- APIs change over time
- Breaking changes require migration periods
- Multiple versions may need to coexist
- Clients shouldn’t break without warning
URI Versioning (Most Common)
/v1/users
/v2/users
Pros: Obvious, cacheable, easy to route. Cons: Changes URI structure, can feel inelegant.
This is the most widely adopted approach for good reason—it’s simple and explicit.
Header Versioning
GET /users
Accept: application/vnd.myapi.v2+json
Pros: Clean URIs, content negotiation. Cons: Hidden from casual inspection, harder to test in browser.
Query Parameter Versioning
/users?version=2
Pros: Simple, explicit. Cons: Caching complications, mixing resource identification with versioning.
Recommendation
Use URI versioning (/v1/) for simplicity and visibility. It’s the most commonly expected pattern.2
Version Lifecycle
- Release v2 while maintaining v1
- Deprecation notice (6-12 months warning)
- Sunset v1 with documented date
- Document migration path clearly
Error Handling
Consistent Error Format
Use the same error structure across your entire API.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request was invalid",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}
}
Error Response Best Practices
- Use appropriate HTTP status code: The code communicates the category.
- Include machine-readable error code: For programmatic handling.
- Include human-readable message: For debugging and display.
- Add field-level details for validation: Help users fix specific issues.
- Include request ID: For debugging and support.
- Never expose stack traces in production: Security risk.
Example Error Responses
Validation Error (422):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Minimum 8 characters required" }
],
"request_id": "req_abc123"
}
}
Not Found (404):
{
"error": {
"code": "NOT_FOUND",
"message": "User with ID 123 not found",
"request_id": "req_def456"
}
}
Rate Limited (429):
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests",
"retry_after": 60,
"request_id": "req_ghi789"
}
}
Pagination
Never return unbounded lists. Pagination protects your server and improves client performance.
Offset-Based Pagination
GET /users?offset=20&limit=10
Response:
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 10,
"total": 153
}
}
Pros: Simple to implement, allows random access (jump to page 5). Cons: Inconsistent with real-time data (items shift as data changes), slow at large offsets.
Cursor-Based Pagination
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTMzfQ",
"has_more": true
}
}
Pros: Consistent results, performant at any scale. Cons: No random access (can only go forward/back), more complex to implement.
Include Pagination Metadata
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 10,
"total": 153,
"total_pages": 16
},
"links": {
"self": "/users?page=2",
"first": "/users?page=1",
"prev": "/users?page=1",
"next": "/users?page=3",
"last": "/users?page=16"
}
}
HATEOAS-style links help clients navigate without hardcoding URLs.
Filtering, Sorting, and Searching
Filtering with Query Parameters
GET /users?status=active
GET /orders?created_after=2024-01-01&created_before=2024-12-31
GET /products?price_min=10&price_max=100
Use descriptive parameter names. created_after is clearer than ca.
Sorting
GET /users?sort=created_at (ascending)
GET /users?sort=-created_at (descending, minus prefix)
GET /products?sort=price,-rating (multiple fields)
The minus prefix for descending is a common convention.
Searching
GET /users?search=john
GET /products?q=wireless+headphones
Field Selection (Sparse Fieldsets)
Allow clients to request only needed fields.
GET /users?fields=id,name,email
GET /orders?fields=id,total,status
Reduces bandwidth and improves performance for mobile clients.
Authentication Patterns
API Keys
GET /users
X-API-Key: sk_live_abc123
Use for: Server-to-server communication, simple integrations. Security: Keep secret, rotate regularly.
Bearer Tokens (JWT)
GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Use for: User authentication, OAuth flows. Security: Tokens expire, include only necessary claims.
OAuth 2.0
Industry standard for delegated authorization. Multiple grant types for different scenarios (authorization code, client credentials, etc.).
Use for: Third-party access, user-authorized integrations.
Best Practices
- Use HTTPS always: Never transmit credentials over unencrypted connections.
- Rotate keys regularly: Automated rotation prevents accumulating risk.
- Implement rate limiting: Protect against abuse.
- Log authentication failures: Detect attacks.
- Expire tokens appropriately: Balance security with user experience.
Request/Response Best Practices
Request Bodies
- Use JSON (
Content-Type: application/json) - Be consistent with naming (
snake_caseorcamelCase, not both) - Validate and sanitize all input
Response Envelope
Consider wrapping responses consistently:
{
"data": { ... },
"meta": {
"request_id": "req_abc123",
"timestamp": "2024-01-15T10:30:00Z"
}
}
Date Formats
Use ISO 8601 for all dates and times:
{
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-01-22T10:30:00Z"
}
Include timezone (Z for UTC or explicit offset).
HATEOAS (Hypermedia Links)
Include links to related resources:
{
"id": 123,
"name": "John",
"links": {
"self": "/users/123",
"orders": "/users/123/orders",
"profile": "/users/123/profile"
}
}
Clients can follow links rather than constructing URLs.
Documentation with OpenAPI
Why OpenAPI?
OpenAPI (formerly Swagger) is the industry standard specification for REST APIs.3
- Auto-generates documentation
- Enables client code generation
- Integrates with testing tools
- Creates interactive API explorers
Basic OpenAPI Structure
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
summary: List users
responses:
'200':
description: Success
Documentation Tools
- Swagger UI: Interactive API documentation
- Redoc: Clean, readable documentation
- Stoplight: Full API design platform
Common Mistakes to Avoid
- Using verbs in URLs: Let HTTP methods be the verbs.
- Inconsistent naming: Pick a convention, enforce it everywhere.
- Not versioning: You’ll need it eventually; plan from the start.
- Poor error messages: Help developers debug; don’t just say “Error.”
- Missing pagination: Unbounded lists kill servers and clients.
- No rate limiting: Protect your infrastructure from abuse.
- Ignoring HTTP semantics: Use proper status codes and methods.
- Breaking changes without versioning: Breaks client applications.
API Design Checklist
Before Building
- Define resources and relationships
- Choose naming conventions (plural nouns, lowercase, hyphens)
- Plan versioning strategy
- Design error response format
- Decide pagination approach
During Building
- Use proper HTTP methods for each operation
- Return appropriate status codes
- Implement consistent error handling
- Add pagination to all list endpoints
- Document with OpenAPI
Before Launch
- Implement rate limiting
- Set up authentication
- Configure monitoring and logging
- Write developer documentation
- Create example code or SDKs
Conclusion
Good API design is about consistency, clarity, and respect for HTTP conventions. Follow established patterns. Be predictable. Make life easy for developers consuming your API.
The investment in thoughtful design pays dividends. APIs with clear contracts attract more users, generate fewer support requests, and evolve more gracefully over time.
Start with the basics: nouns for resources, proper HTTP methods, meaningful status codes, consistent error handling. Build from there.
Further Reading
- Related: Modern Tech Stack Guide
- Related: Database Selection Guide
- Related: Authentication Implementation Guide
- Microsoft Azure - Web API Design Best Practices
- Google API Design Guide
- OpenAPI Specification
References
Footnotes
-
Fielding, Roy Thomas. “Architectural Styles and the Design of Network-based Software Architectures.” Doctoral dissertation, University of California, Irvine, 2000. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm ↩
-
Microsoft. “Web API design best practices.” https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design ↩
-
OpenAPI Initiative. “OpenAPI Specification.” https://spec.openapis.org/oas/latest.html ↩