REST API Best Practices: A Developer’s Guide to Building Reliable APIs
Quick reference
-
Use nouns, not verbs. URLs represent resources, and HTTP methods define what happens.
-
Use correct HTTP methods. GET, POST, PUT, PATCH, and DELETE each have clear semantics.
-
Return meaningful status codes. Clients should understand outcomes without parsing bodies.
-
Version your API. Plan versioning from the start to manage changes without breaking clients.
-
Secure with HTTPS and authentication. Never transmit sensitive data over unencrypted connections.
-
Paginate large collections. Break large result sets into manageable chunks for better performance.
-
Standardize errors. Consistent error shapes save debugging time.
-
Design for idempotency. Retries should not create duplicates.
-
Observe everything. Logs, metrics, and request IDs make issues diagnosable.
-
Keep documentation executable. Examples should match reality.
A well-designed REST API is intuitive, secure, scalable, and easy to maintain. In production, reliability starts with consistency. Whether you’re designing your first REST API or refining an existing one, following proven best practices helps teams avoid breaking clients, reduce support overhead, and make APIs easier to adopt.
This guide covers essential REST API design principles, along with practical examples that you can apply immediately.
Use consistent and meaningful resource naming
Resource naming sets expectations for how your API behaves. Clear, consistent names make endpoints easier to understand and less likely to be misused.
Use nouns, not verbs
REST endpoints should represent resources (things) rather than actions. HTTP methods already describe the action, so your URLs should focus on what you’re working with.
Good practice
GET /users
POST /orders
DELETE /products/123
Bad practice
GET /getUsers
POST /createOrder
DELETE /deleteProduct/123
Keep URLs predictable
Use plural nouns for collections and follow consistent patterns throughout your API. Developers should be able to predict endpoint names based on patterns they’ve already seen.
GET /users → List all users
GET /users/123 → Get specific user
POST /users → Create new user
PUT /users/123 → Update user
DELETE /users/123 → Delete user
For nested resources, reflect relationships in the URL structure, but avoid going too deep.
GET /users/123/orders → Get orders for a specific user
GET /orders/456/items → Get items in a specific order>/code>
In production: More than two levels of nesting often signals a resource modeling problem. Consider flattening or introducing a dedicated endpoint.
Use proper HTTP methods
HTTP methods define the action being performed. Using them correctly makes your API predictable and allows clients to understand what each request will do.
GET retrieves data
Use GET for read-only operations that don’t modify server state. GET requests should be safe to call repeatedly without side effects.
GET /products?category=electronics&limit=20
GET requests are cacheable and can include query parameters for filtering, sorting, and pagination. Never use GET for operations that change data.
POST creates new resources
Use POST when creating new resources or triggering actions that change server state. The server typically assigns the new resource’s identifier.
POST /users
Content-Type: application/json
{
"name": "Phil Ostman",
"email": "[email protected]"
}
Return 201 Created and include a Location header pointing to the new resource.
PUT updates or replaces resources
Use PUT for full updates of existing resources. PUT is idempotent, meaning multiple identical requests produce the same result.
PUT /users/123
Content-Type: application/json
{
"name": "Phil Ostman",
"email": "[email protected]",
"role": "Developer"
}
PATCH makes partial updates
Use PATCH when you only need to modify specific fields without sending the entire resource.
PATCH /users/123
Content-Type: application/json
{
"email": "[email protected]"
}
PATCH is particularly useful for large resources where sending the complete object would be inefficient.
DELETE removes resources
Use DELETE to remove resources from the server. Like PUT, DELETE should be idempotent.
DELETE /users/123
After a successful deletion, return 204 No Content or 200 OK with details about what was deleted.
Implement proper HTTP status codes
HTTP status codes communicate the result of each request. Using them correctly helps clients handle responses appropriately without parsing response bodies.
Success codes (2xx)
-
200 OK: Request succeeded, response contains data
-
201 Created: New resource created successfully
-
204 No Content: Request succeeded, no response body
-
202 Accepted: Request accepted for processing but not completed
Client error codes (4xx)
-
400 Bad Request: Invalid request format or parameters
-
401 Unauthorized: Authentication required or failed
-
403 Forbidden: Authenticated but lacks permissions
-
404 Not Found: Resource doesn’t exist
-
409 Conflict: Request conflicts with the current state
-
422 Unprocessable Entity: Valid format, but semantic errors
Server error codes (5xx)
-
500 Internal Server Error: Generic server failure
-
502 Bad Gateway: Invalid response from upstream server
-
503 Service Unavailable: Server temporarily unavailable
-
504 Gateway Timeout: Upstream server didn’t respond
In production: Inconsistent status codes are one of the most common causes of client-side bugs.
Provide meaningful, consistent error responses
Status codes tell clients what went wrong, but error response bodies explain why and how to fix it. Good error responses include enough detail for developers to troubleshoot issues without exposing sensitive information.
Standardize error structure
Use a standard format for all error responses across your API.
{
"error": {
"code": "INVALID_EMAIL",
"message": "The email address format is invalid",
"details": "Email must contain an @ symbol and valid domain",
"field": "email"
}
}
Use stable, machine-readable error codes along with human-readable messages.
Return all validation errors at once
When validation fails, tell users exactly what’s wrong and how to fix it.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Email address is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
]
}
}
This saves clients from slow, iterative fixes.
Version your API deliberately
APIs evolve over time, and versioning allows you to make improvements without breaking existing clients. Plan your versioning strategy from the start, even if you only have one version initially.
URI versioning
Include the version number in the URL path. This approach is explicit and easy to understand.
GET /v1/users
GET /v2/users
URI versioning makes it immediately clear which version a client is using and allows different versions to coexist on the same server.
Header versioning
Pass the version in a custom header. This keeps URLs clean but requires clients to set headers correctly.
GET /users
Accept: application/vnd.api.v1+json
Header versioning works well when you want cleaner URLs and have clients sophisticated enough to manage headers.
When to create a new version
Introduce a new version for breaking changes: removed fields, type changes, or auth changes. Non-breaking additions usually don’t require a new version.
Paginate large collections
Returning large datasets in a single response can create performance problems and a negative user experience. Pagination breaks large result sets into manageable chunks.
Page number pagination
Use page numbers for a more traditional pagination experience.
GET /products?page=3&per_page=20
Include pagination metadata in responses to help clients navigate result sets.
{
"data": [...],
"pagination": {
"current_page": 3,
"total_pages": 15,
"total_items": 287,
"per_page": 20
}
}
Limit and offset pagination
Allow clients to specify how many results they want and where to start.
GET /products?limit=20&offset=40
This approach is straightforward but can miss items if the collection changes between requests.
Cursor-based pagination
Use cursor tokens that point to specific positions in result sets.
GET /products?limit=20&cursor=eyJpZCI6MTIzfQ
Return a next cursor in the response that clients can use to fetch the next page.
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}
In production: Cursor-based pagination is more reliable for feeds and high-write datasets.
Support filtering, sorting, and searching
Beyond pagination, give clients control over what data they receive and how it’s organized.
Filtering
Allow clients to filter results using query parameters.
GET /products?category=electronics&price_min=100&price_max=500
Use clear parameter names that make the filtering criteria obvious.
Sorting
Enable sorting with parameters that specify the field and direction.
GET /products?sort=price:asc
GET /users?sort=created_at:desc
Support multiple sort criteria when appropriate.
GET /products?sort=category:asc,price:desc
Searching
Provide search functionality for text-based queries.
GET /products?search=wireless keyboard
GET /articles?q=api+design
Consider implementing more sophisticated search features like fuzzy matching or field-specific searches for complex use cases.
Secure your API with proper authentication
Security is non-negotiable for APIs. Choose authentication methods appropriate for your use case and implement them consistently.
API keys
Simple authentication using unique keys passed in headers.
GET /api/users
X-API-Key: sk_live_abc123xyz789
API keys are well-suited for server-to-server communication, but they shouldn’t be the only security layer for sensitive operations.
Bearer tokens (OAuth 2.0)
Use OAuth 2.0 for applications requiring user-specific authentication and authorization.
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Bearer tokens support time-limited access and can include scope restrictions to limit the actions that authenticated clients can perform.
Always use HTTPS
Encrypt all API traffic with HTTPS to protect authentication credentials and sensitive data in transit. HTTP transmits data in plain text, exposing it to interception.
Rate limiting
Implement rate limiting to prevent abuse and ensure fair resource allocation. Return appropriate headers to inform clients about their usage.
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1640995200
When rate limits are exceeded, return a 429 Too Many Requests status with information about when the client can retry.
Provide comprehensive documentation
Documentation transforms your API from a black box into a tool that developers can use with confidence. Good documentation includes examples, explains edge cases, and stays current with API changes.
Document all endpoints
For each endpoint, document the HTTP method, URL, required parameters, request body format, response format, status codes, and authentication requirements.
Include working examples
Show actual requests and responses that developers can copy and test.
Request:
POST /api/users
Content-Type: application/json
Authorization: Bearer token123
{
"name": "Phil Ostman",
"email": "[email protected]"
}
Response:
HTTP/1.1 201 Created
Location: /api/users/12345
{
"id": "12345",
"name": "Phil Ostman",
"email": "[email protected]",
"created_at": "2024-01-15T10:30:00Z"
}
Explain error cases
Document common error scenarios and how clients should handle them. Include examples of error responses and their meanings.
Tools like Postman make it easy to create interactive documentation that developers can use to explore your API without needing to write code.
Design for idempotency
Idempotent operations produce the same result regardless of the number of times they’re executed. Network issues and retries are inevitable, and idempotency prevents duplicate actions.
Safe methods (GET, HEAD) are naturally idempotent because they don’t modify state. PUT and DELETE should also be idempotent by design.
"PUT /users/123 → Updates user 123, same result if called multiple times
DELETE /users/123 → Deletes user 123, subsequent calls return 404
POST is typically not idempotent because each request might create a new resource. To make POST operations safer, implement idempotency keys.
POST /payments
Idempotency-Key: unique-key-abc123
Content-Type: application/json { "amount": 100.00, "currency": "USD" } >
The server stores the idempotency key and returns the same response if the request is repeated with the same key, preventing duplicate payments.
Handle CORS properly
Cross-Origin Resource Sharing (CORS) allows web applications from different domains to access your API. Configure CORS headers to specify which origins can access your API.
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
For public APIs, you might use a wildcard to allow all origins, but this reduces security. For private APIs, explicitly list allowed origins.
Use appropriate content types
Specify content types clearly so clients know how to parse responses and servers know how to interpret requests.
JSON is the standard
JSON has become the standard for REST APIs. Use application/json as your primary content type.
Content-Type: application/json
Accept: application/json
Support content negotiation
Allow clients to request different formats when appropriate.
GET /api/report
Accept: application/pdf
The server can return different representations based on the Accept header, though JSON should remain your primary format.
Keep responses focused and efficient
Design response bodies to include what clients need without excess data. Large responses slow down transfers and force clients to parse unnecessary information.
Use field selection
Allow clients to specify which fields to include.
GET /users/123?fields=name,email
This reduces payload size and improves performance, particularly for mobile clients or those with slow connections.
Avoid deeply nested structures
Keep response structures relatively flat. Deep nesting makes responses harder to parse and navigate.
Good practice
{
"user": {
"id": "123",
"name": "Phil Ostman"
},
"organization_id": "org-456",
"organization_name": "Tech Corp"
}
Bad practice
{
"user": {
"id": "123",
"name": "Phil Ostman",
"organization": {
"id": "org-456",
"name": "Tech Corp",
"details": {
"industry": "Technology",
"settings": {
"timezone": "UTC"
}
}
}
}
}
In production: Deep nesting often leaks internal data models into public APIs.
Observe, log, and monitor API usage
Reliable APIs are observable APIs. Comprehensive logging helps you understand how your API is being used, diagnose problems, and identify performance bottlenecks:
-
Log request method, endpoint, status code, and latency
-
Track error rates and slow endpoints
-
Alert on anomalies
Use request IDs
Include unique identifiers in responses that clients can reference when reporting issues.
X-Request-ID: f47ac10b-58cc-4372-a567-0e02b2c3d479
This makes it easy to correlate client reports with server logs and trace requests through your systems.
Plan for backward compatibility
Once clients depend on your API, changes become risky. Design with future evolution in mind.
Add, don’t remove
When possible, add new fields or endpoints rather than removing or changing existing ones. Clients that ignore new fields won’t break, but removing fields will.
Deprecation process
When you must remove or change functionality, follow a clear deprecation process:
- Announce changes early
- Provide migration guidance
- Support old and new versions in parallel
Include deprecation warnings in responses to notify clients they’re using outdated endpoints.
Deprecated: true
X-API-Warn: This endpoint is deprecated and will be removed on 2025-06-01. Use /v2/users instead.
Test APIs continuously
Rigorous testing catches issues before they affect clients. Test APIs across multiple dimensions to ensure reliability, beyond the happy paths:
- Authentication failures
- Validation errors
- Rate limiting
- Edge cases and retries
Unit tests verify that individual components work correctly. Integration tests confirm components work together properly. End-to-end tests validate complete workflows from the client’s perspective.
Common mistakes to avoid
Understanding pitfalls helps you avoid them in your designs.
Using GET for state changes
Never use GET for operations that modify data. GET requests should be safe and idempotent.
Bad practice
GET /users/123/delete
GET /cart/add?product_id=789
Good practice
DELETE /users/123
POST /cart/items
Exposing implementation details
Keep internal system details out of your API design. URLs and field names should reflect domain concepts, not database tables or internal architecture.
Bad practice
GET /user_table_v2/query?db_id=123
Good practice
GET /users/123
Inconsistent naming and conventions
Use consistent patterns throughout your API. If you use snake_case for field names in one endpoint, use it everywhere. If you pluralize collection names, do it consistently.
Final thoughts
APIs can fail for many reasons, but one common culprit is drift. URLs, examples, error shapes, and documentation slowly fall out of sync with the published version.
Treat all of your artifacts, including examples, tests, documentation, and monitoring, as part of the API contract. When they stay connected and executable, teams can catch breaking changes earlier and ship more reliable APIs.


What do you think about this topic? Tell us in a comment below.