Introduction
RESTful APIs have become the backbone of modern web applications, powering everything from mobile apps to microservices architectures. While the REST architectural style provides a solid foundation, the difference between a good API and a great one lies in the attention to detail during design and implementation. Production-ready APIs must handle millions of requests, integrate with diverse clients, and evolve without breaking existing integrations.
This guide focuses on practical best practices that separate amateur APIs from professional ones. These practices have been refined through years of industry experience and represent the consensus of API engineering teams at scale. Whether you are building your first API or looking to improve an existing one, these guidelines will help you create interfaces that developers love to use.
The goal is not to follow REST puritanically but to create APIs that are intuitive, performant, and maintainable. Sometimes this means bending strict REST principles for practical reasons; the key is understanding when and why to do so.
URL Design and Naming Conventions
Resource Naming Principles
The URL structure of your API is its public face. Well-designed URLs are intuitive, consistent, and reflect the domain they represent. Poor URL design leads to confusion, inconsistent implementations, and difficult-to-maintain code.
Use Nouns for Resources, Not Verbs. This fundamental principle aligns with HTTP’s semantic methods. The URL identifies what you are acting upon, while the HTTP method identifies the action. Compare these approaches:
# Anti-pattern: Verb in URL
POST /api/createUser
POST /api/users/create
GET /api/getUserById/123
GET /api/users/get/123
# Correct approach: Noun-based resources
POST /api/users
GET /api/users/123
DELETE /api/users/123
The correct approach leverages HTTP methods for their intended purpose, resulting in cleaner URLs and more consistent client code.
Use Plural Nouns for Collections. When a resource represents a collection, use plural form consistently:
GET /api/users # List all users
POST /api/users # Create a new user
GET /api/users/123 # Get specific user
PUT /api/users/123 # Update user
DELETE /api/users/123 # Delete user
Some APIs use singular nouns for singleton resources (like /api/user/profile for the current user’s profile), which is acceptable when the resource is clearly singular.
Reflect Hierarchical Relationships in URLs. When one resource belongs to another, this relationship should be reflected in the URL structure:
GET /api/users/123/posts # Posts by user 123
GET /api/users/123/posts/456 # Specific post by user
GET /api/users/123/posts/456/comments # Comments on a post
However, avoid going more than two levels deep. If you find yourself with URLs like /api/teams/123/members/456/projects/789/tasks/321, consider whether the resource should be accessed directly at /api/tasks/321 with appropriate filtering.
Use Lowercase with Hyphens. URL paths should be lowercase, and multi-word paths should use hyphens (not underscores) for readability:
# Good
/api/user-profiles
/api/order-items
/api/content-management
# Avoid
/api/user_profiles # Underscores harder to read
/api/UserProfiles # Case inconsistency
Query Parameters for Filtering and Operations
Query parameters should be used for filtering, sorting, pagination, and optional modifiers. They should not be used for core resource identification.
Filtering Pattern:
GET /api/posts?status=published&author=456&year=2026
GET /api/products?category=electronics&minPrice=100&brand=Apple
GET /api/orders?customerId=123&status=shipped
Sorting Pattern:
GET /api/posts?sort=-createdAt,title # Descending by createdAt, then ascending by title
GET /api/users?sort=name # Ascending by name
Boolean Flags:
GET /api/posts?includeComments=true&draft=false
GET /api/products?featured=true&inStock=true
Field Selection (Sparse Fieldsets):
GET /api/users?fields=id,name,email
GET /api/posts?fields=title,excerpt,slug
This pattern allows clients to request only the fields they need, reducing bandwidth and improving performance.
Request and Response Design
Request Body Design
Request bodies should be well-structured, consistent, and predictable. The design should minimize client errors while providing clear validation feedback.
Envelope Pattern for POST/PUT:
{
"data": {
"type": "users",
"attributes": {
"email": "[email protected]",
"name": "John Doe",
"profile": {
"bio": "Software engineer",
"location": "San Francisco"
}
}
}
}
This JSON:API-inspired structure provides clarity about what type of resource is being created and separates attributes from metadata. However, for simpler APIs, a direct object may be preferable:
{
"email": "[email protected]",
"name": "John Doe",
"bio": "Software engineer",
"location": "San Francisco"
}
Partial Update Payloads (PATCH):
{
"email": "[email protected]"
}
For PATCH requests, only include the fields being modified. This is more efficient than sending the entire resource and makes the intent clear.
Response Body Design
Response bodies should be consistent, informative, and appropriately structured for the data they contain.
Single Resource Response:
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"email": "[email protected]",
"name": "John Doe",
"createdAt": "2026-01-15T10:30:00Z"
},
"links": {
"self": "/api/users/123"
}
}
}
Collection Response:
{
"data": [
{
"id": "123",
"type": "users",
"attributes": {
"email": "[email protected]",
"name": "John Doe"
}
},
{
"id": "456",
"type": "users",
"attributes": {
"email": "[email protected]",
"name": "Jane Smith"
}
}
],
"meta": {
"total": 150,
"count": 2,
"page": 1,
"perPage": 2
},
"links": {
"first": "/api/users?page=1",
"next": "/api/users?page=2",
"last": "/api/users?page=75"
}
}
Error Response:
{
"errors": [
{
"id": "req-abc123",
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Validation Failed",
"detail": "The email field must be a valid email address",
"source": {
"pointer": "/data/attributes/email"
}
}
]
}
Error responses should follow a consistent structure that includes an error identifier, HTTP status code, machine-readable error code, human-readable title, detailed description, and source location.
HTTP Status Codes and Error Handling
Status Code Reference
| Code | Description | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH requests |
| 201 | Created | Resource successfully created via POST |
| 204 | No Content | Successful DELETE, no response body |
| 301 | Moved Permanently | Resource relocated |
| 400 | Bad Request | Invalid request syntax or parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not permitted |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Resource state conflict (duplicate email) |
| 422 | Unprocessable Entity | Valid syntax but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server failure |
| 503 | Service Unavailable | Temporarily overloaded or down |
Standard Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
}
],
"requestId": "req_abc123xyz"
}
}
Error Response Examples
// Resource not found
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 123 not found",
"resource": "User",
"resourceId": "123"
}
}
// Authentication failure
{
"error": {
"code": "AUTHENTICATION_FAILED",
"message": "Invalid API key",
"help": "Provide a valid Authorization header"
}
}
Pagination, Filtering, and Sorting
Pagination
Always paginate collection endpoints:
GET /users?page=1&limit=20
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
Cursor-based pagination for real-time data:
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"previousCursor": "eyJpZCI6OTB9",
"hasMore": true
}
}
Use offset for page jumping; cursor for large or real-time datasets.
Filtering
# Single filter
GET /users?status=active
# Range filters
GET /products?price_min=10&price_max=100
GET /orders?created_after=2026-01-01
# In/exclude filters
GET /users?role_in=admin,moderator
GET /users?status_ne=deleted
Sorting
# Single sort
GET /users?sort=created_at
# Descending sort
GET /users?sort=created_at,desc
# Multiple sort fields
GET /users?sort=status,asc;created_at,desc
Field Selection
# Return only specific fields
GET /users?fields=id,name,email
# Exclude specific fields
GET /users?exclude=password,token
Documentation Best Practices
Using OpenAPI
OpenAPI (formerly Swagger) is the standard for describing REST APIs. It enables auto-generated docs, SDKs, and request validation.
openapi: 3.0.3
info:
title: My API
version: '1.0.0'
paths:
/users/{id}:
get:
summary: Get a user
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
API Documentation Checklist
- Quickstart: authenticate and make first request in under 5 minutes
- Reference: each endpoint with method, path, parameters, status codes
- Examples: request/response samples for success and error cases
- Authentication guide: how to obtain and refresh tokens
- Change log and deprecation schedule
- Rate limit documentation
Deployment Architecture
Client (Browser/Mobile) -> Load Balancer (Nginx) -> API Gateway -> Backend Servers -> Database
|
-> Authentication Service
-> Caching Layer (Redis)
- Use containerization (Docker) for consistent environments
- Implement monitoring (Prometheus) and logging
- Secure with HTTPS, API keys, and OAuth
API Versioning Strategies
Versioning Approaches
API versioning is essential for evolving APIs without breaking existing clients. Several strategies exist, each with trade-offs.
URI Path Versioning (Most Common):
GET /api/v1/users
GET /api/v2/users
This approach is simple, explicit, and cache-friendly. The version is immediately visible in the URL, making it easy to test different versions.
Header-Based Versioning:
GET /api/users
Accept-Version: v1
Header-based versioning keeps URLs clean but requires clients to set headers correctly.
Query Parameter Versioning:
GET /api/users?version=1
Query parameter versioning is easy to implement but pollutes the URL space.
Versioning Best Practices
Never Break Existing Versions. Once an API version is released, it should be supported indefinitely or with clear deprecation timelines. Breaking changes should only occur in new versions.
Deprecate Gracefully. When retiring an API version, provide ample notice:
Deprecation: true
Sunset: Mon, 01 Jan 2027 00:00:00 GMT
Link: </docs/v2-migration>; rel="deprecation"; type="text/html"
Limit Active Versions. Most organizations should support at most 2-3 active versions at any time.
Version Major Changes Only. New versions should only be created for breaking changes. Non-breaking additions can be added to existing versions.
Breaking Changes Include:
- Removing or renaming endpoints
- Removing or renaming request/response fields
- Changing field types
- Changing authentication requirements
Non-Breaking Changes Include:
- Adding new optional request fields
- Adding new response fields
- Adding new endpoints
- Adding new enum values
Performance Optimization
Caching Strategies
Effective caching dramatically improves API performance and reduces server load.
Cache-Control Headers:
Cache-Control: max-age=3600, public
Cache-Control: no-cache, no-store, must-revalidate
Cache-Control: private, max-age=600
max-age: Number of seconds the response is freshpublic: Response can be cached by shared cachesprivate: Response can only be cached by the clientno-cache: Must validate with server before using cached response
ETags for Conditional Requests:
ETag: "abc123"
If-None-Match: "abc123" # Returns 304 if unchanged
ETags enable efficient conditional requests. When a client has a cached response, it can send the ETag to check if the resource has changed. If unchanged, the server returns 304 Not Modified without the response body.
Compression
Response compression reduces bandwidth and improves perceived performance.
Gzip Compression:
Accept-Encoding: gzip, deflate
Content-Encoding: gzip
Enable compression on your server for all text-based responses (JSON, XML, HTML).
Batch Operations
Reducing the number of round trips improves performance.
Batch Create:
POST /api/users/batch
[
{ "email": "[email protected]", "name": "User 1" },
{ "email": "[email protected]", "name": "User 2" },
{ "email": "[email protected]", "name": "User 3" }
]
Compound Documents (Includes):
GET /api/posts?include=author,comments
This pattern allows clients to fetch related resources in a single request, avoiding the N+1 query problem.
Idempotency and Safety
Understanding Idempotency
Idempotency is a crucial property for production APIs. An idempotent operation can be applied multiple times without changing the result beyond the initial application.
Idempotent Methods:
- GET: Safe and idempotent
- PUT: Idempotent (replacing a resource multiple times has the same effect)
- DELETE: Idempotent (deleting a resource multiple times is equivalent to once)
Non-Idempotent Methods:
- POST: Not idempotent (creating a resource multiple times creates multiple resources)
Implementing Idempotency
For POST endpoints that create resources, provide an idempotency key header:
Idempotency-Key: abc123-uuid-here
The server stores the idempotency key with the response for a period (typically 24-48 hours). If a client retries with the same key, the server returns the cached response instead of creating a duplicate resource.
Best Practices Summary
Do’s
- Use nouns for resource names (plural)
- Use proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Return appropriate status codes
- Implement pagination for collections
- Version your API from day one
- Use consistent error response formats
- Document your API (OpenAPI spec)
- Implement rate limiting
- Validate all input on the server side
- Always use HTTPS in production
Don’ts
- Use verbs in URLs (
/getUser,/createUser) - Return 200 for errors
- Expose sensitive data in error messages
- Use generic error messages
- Skip versioning
- Return different response structures for the same status code
- Use deep nesting beyond two levels
- Store secrets in source code or logs
Conclusion
Building production-ready RESTful APIs requires attention to detail across multiple dimensions. From URL design to error handling, caching to versioning, every decision impacts the developer experience and operational characteristics of your API.
The best practices outlined in this guide represent accumulated wisdom from building and operating APIs at scale. They are not rigid rules but guidelines that should be applied with judgment.
Remember that an API is a contract with its users. Once published, changes become increasingly difficult. Invest in thoughtful design upfront, document your decisions, and maintain clear communication with API consumers about changes and deprecations.
Comments