REST API Best Practices: A Developer’s Guide to Building Reliable APIs

REST API Best Practices: A Developer’s Guide to Building Reliable APIs

User Avatar

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.


Try Postman today →

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.

Tags:

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

Comment

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.