T ToolHub
Development

REST API Design Best Practices for Modern Web Development

Updated March 2026 · 14 min read

1. What is REST?

REST, or Representational State Transfer, is an architectural style for designing networked applications. It was first introduced by Roy Fielding in his doctoral dissertation in 2000 and has since become the dominant paradigm for building web APIs. Rather than being a strict protocol like SOAP, REST is a set of architectural constraints that, when applied together, produce scalable, maintainable, and performant distributed systems.

Fielding defined six key constraints that characterize a truly RESTful architecture. Understanding these constraints is essential before diving into the practical aspects of API design, because they inform every decision you will make when structuring your endpoints, choosing response formats, and handling state.

Client-Server Separation: The client and server must be independent of each other. The client does not need to know anything about business logic or data storage, and the server does not need to know anything about the user interface. This separation of concerns allows both sides to evolve independently. A mobile app, a web frontend, and a CLI tool can all consume the same API without the server needing any awareness of its consumers.

Statelessness: Each request from a client to the server must contain all the information needed to understand and process the request. The server does not store any client session state between requests. This means authentication tokens, context parameters, and any other relevant data must be sent with every request. Statelessness dramatically improves scalability because any server instance can handle any request without needing access to session stores.

Cacheability: Responses must explicitly define themselves as cacheable or non-cacheable. When a response is cacheable, the client (or intermediary proxies) can reuse that response for subsequent equivalent requests. Proper cache control headers such as Cache-Control, ETag, and Last-Modified reduce server load and improve response times significantly.

Uniform Interface: This is arguably the most fundamental constraint and the one that distinguishes REST from other architectural styles. It mandates that resources are identified by URIs, resources are manipulated through their representations, messages are self-descriptive, and hypermedia drives application state (HATEOAS). A uniform interface simplifies the overall architecture and decouples implementation from the services provided.

Layered System: The architecture should be composed of hierarchical layers, where each layer only interacts with its immediate neighbor. A client cannot tell whether it is connected directly to the end server or to an intermediary. This allows for load balancers, caches, security gateways, and other middleware to be inserted transparently into the communication chain.

Code on Demand (Optional): Servers can optionally extend client functionality by transferring executable code, such as JavaScript. This is the only optional constraint in REST and is rarely discussed in the context of API design, but it is part of the original specification.

2. Resource Naming Conventions

One of the first things developers encounter when designing a REST API is deciding how to name their endpoints. Good resource naming is the cornerstone of an intuitive API. The fundamental rule is that URIs should represent resources (nouns), not actions (verbs). The HTTP method already conveys the action, so the URI should focus solely on identifying what is being acted upon.

Use nouns, not verbs. Your endpoints should describe the resource, not the operation. Instead of /getUsers or /createUser, simply use /users. The HTTP method determines whether you are reading, creating, updating, or deleting the resource.

Use plural nouns for collections. Collections should always use plural forms for consistency. Use /users rather than /user, /articles rather than /article. This creates a natural mental model: the collection endpoint returns many items, and appending an identifier returns a single item from that collection.

Use hierarchical paths to express relationships. When resources have natural parent-child relationships, express them through nesting. This communicates ownership and context clearly.

GET  /users                      # List all users
GET  /users/42                   # Get user with ID 42
GET  /users/42/orders            # List orders for user 42
GET  /users/42/orders/7          # Get order 7 for user 42
POST /users/42/orders            # Create a new order for user 42

However, avoid nesting more than two levels deep. Deeply nested URLs become unwieldy and harder to manage. If you find yourself writing /users/42/orders/7/items/3/reviews, consider flattening part of the hierarchy by providing direct access to sub-resources like /order-items/3/reviews.

Additional naming guidelines to keep in mind:

  • Use lowercase letters and hyphens for multi-word resources: /training-plans, not /trainingPlans or /training_plans.
  • Do not include file extensions in URIs. Use content negotiation via the Accept header instead.
  • Avoid trailing slashes. Treat /users and /users/ as the same resource by redirecting one to the other.
  • Use query parameters for filtering and sorting, not separate endpoints.

3. HTTP Methods

HTTP methods (also called verbs) define the action to be performed on a resource. Using the correct method for each operation is critical for a well-designed API, as it provides semantic meaning that clients, caches, and intermediaries can rely on.

GET — Retrieve a representation of a resource. GET requests must be safe (they do not modify state) and idempotent (making the same request multiple times produces the same result). Use GET for fetching individual resources or collections. Never use GET to create, update, or delete data.

POST — Create a new resource or trigger a process. POST is neither safe nor idempotent. Each POST request may create a new resource, so sending the same request twice could result in two separate resources. Use POST when the server determines the URI of the newly created resource, and return a 201 Created status with a Location header pointing to the new resource.

PUT — Replace an entire resource with the provided representation. PUT is idempotent: sending the same PUT request multiple times has the same effect as sending it once. Use PUT when the client sends a complete representation of the resource. If the resource does not exist, PUT can create it at the specified URI.

PATCH — Partially update a resource. PATCH allows clients to send only the fields that need to change, rather than the entire resource. It is not guaranteed to be idempotent depending on the patch format used. PATCH is ideal for scenarios where you want to update a user's email without resending their entire profile.

DELETE — Remove a resource. DELETE is idempotent: deleting a resource that has already been deleted should return the same result (typically 204 No Content or 404 Not Found). Use DELETE to remove resources permanently or to mark them for soft deletion.

GET    /articles          → List all articles         (safe, idempotent)
GET    /articles/15       → Get article 15            (safe, idempotent)
POST   /articles          → Create a new article      (unsafe, not idempotent)
PUT    /articles/15       → Replace article 15        (unsafe, idempotent)
PATCH  /articles/15       → Partial update article 15 (unsafe, not guaranteed idempotent)
DELETE /articles/15       → Delete article 15         (unsafe, idempotent)

Understanding idempotency is particularly important when dealing with unreliable networks. If a client is unsure whether a PUT request succeeded, it can safely retry the request without worrying about creating duplicate resources. This is not the case with POST, which is why many APIs implement idempotency keys for POST requests to safely handle retries.

4. HTTP Status Codes

HTTP status codes communicate the result of the server's attempt to fulfill a request. Using the right status codes is essential for building APIs that clients can interact with reliably and programmatically. Here are the status codes every API developer should know and use correctly.

2xx — Success:

  • 200 OK — The request succeeded. Use for successful GET, PUT, PATCH, and DELETE requests that return a response body.
  • 201 Created — A new resource was successfully created. Always use this for successful POST requests that create a resource. Include a Location header with the URI of the new resource.
  • 204 No Content — The request succeeded, but there is no content to return. Commonly used for successful DELETE requests or PUT/PATCH requests that do not return the updated resource.

4xx — Client Errors:

  • 400 Bad Request — The server cannot process the request due to a client error, such as malformed JSON, missing required fields, or invalid data types.
  • 401 Unauthorized — Authentication is required but was not provided or is invalid. The client must authenticate before accessing the resource.
  • 403 Forbidden — The client is authenticated but does not have permission to access the requested resource. Unlike 401, re-authenticating will not help.
  • 404 Not Found — The requested resource does not exist. Use this for missing resources, not for authorization failures (which should use 403).
  • 409 Conflict — The request conflicts with the current state of the resource. Common for duplicate entries, version conflicts, or concurrent modification issues.
  • 422 Unprocessable Entity — The request is syntactically valid but semantically incorrect. Use this for validation errors where the JSON structure is correct but the values fail business rules.

5xx — Server Errors:

  • 500 Internal Server Error — An unexpected condition prevented the server from fulfilling the request. This should never be returned intentionally; it indicates a bug or unhandled exception on the server.

A common anti-pattern is returning 200 OK for every response and embedding the actual status in the response body. This forces clients to parse the body to determine if the request succeeded, defeating the purpose of HTTP status codes. Always use the appropriate status code in the HTTP response line.

5. Request and Response Design

Consistent request and response formats are crucial for developer experience. JSON has become the de facto standard for REST APIs, and your API should accept and return JSON by default. Always set the Content-Type: application/json header in responses.

Use consistent field naming. Choose a naming convention — camelCase or snake_case — and stick with it throughout your entire API. Mixing conventions is confusing and error-prone. Most JavaScript-centric APIs use camelCase, while Python-centric APIs often use snake_case.

Envelope vs. Direct responses: There are two common approaches for structuring responses. The envelope approach wraps the data in a container object, while the direct approach returns the data at the top level.

// Envelope approach
{
  "status": "success",
  "data": {
    "id": 42,
    "name": "Jane Smith",
    "email": "jane@example.com"
  },
  "meta": {
    "requestId": "req_abc123"
  }
}

// Direct approach
{
  "id": 42,
  "name": "Jane Smith",
  "email": "jane@example.com"
}

The envelope approach adds consistency and a place for metadata, but adds verbosity. The direct approach is cleaner and works well with HTTP status codes and headers for metadata. Many modern APIs favor the direct approach for single resources and a light envelope for collections that include pagination metadata.

Pagination in responses is essential for any endpoint that returns a collection. Never return unbounded collections as they can overwhelm both server and client. Include pagination metadata alongside the data:

{
  "data": [
    { "id": 1, "title": "First Post" },
    { "id": 2, "title": "Second Post" }
  ],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "totalPages": 5,
    "totalItems": 98
  }
}

When crafting request bodies, accept only the fields that can be set by the client. Do not accept server-generated fields like id, createdAt, or updatedAt on creation or update operations. Always validate all incoming data and return clear errors for malformed or invalid requests.

6. Filtering, Sorting, and Pagination

Real-world APIs need to support filtering, sorting, and pagination to be practical. These operations should always be implemented through query parameters on collection endpoints, keeping the base URL clean and focused on the resource itself.

Filtering allows clients to narrow down results based on specific criteria. Use simple query parameters that map to resource fields:

GET /articles?status=published
GET /articles?status=published&category=technology
GET /articles?createdAfter=2025-01-01&createdBefore=2025-12-31
GET /users?role=admin&active=true

For more advanced filtering, some APIs adopt a filter syntax like filter[field][operator]=value. This approach is more flexible but adds complexity for both the server and client.

Sorting allows clients to control the order of results. The most common convention uses a sort query parameter with field names, using a prefix to indicate direction:

GET /articles?sort=createdAt        # Ascending by creation date
GET /articles?sort=-createdAt       # Descending by creation date
GET /articles?sort=-createdAt,title # Descending by date, then ascending by title

Pagination can be implemented using two primary strategies:

Offset-based pagination uses page and perPage (or limit and offset) parameters. It is simple to implement and allows jumping to any page directly. However, it has performance issues with large datasets (the database still needs to skip over all preceding rows) and produces inconsistent results when data is being inserted or deleted concurrently.

Cursor-based pagination uses an opaque token (cursor) that points to a specific position in the dataset. The server returns a cursor with each page, and the client uses it to request the next page. This approach is more performant at scale and produces consistent results regardless of concurrent modifications, but cannot jump to arbitrary pages.

// Cursor-based pagination response
{
  "data": [...],
  "cursors": {
    "next": "eyJpZCI6MTAwfQ==",
    "previous": "eyJpZCI6ODF9"
  },
  "links": {
    "next": "/articles?cursor=eyJpZCI6MTAwfQ==&limit=20",
    "previous": "/articles?cursor=eyJpZCI6ODF9&limit=20"
  }
}

HATEOAS links (Hypermedia as the Engine of Application State) provide navigational links within responses, allowing clients to discover actions and related resources dynamically. While full HATEOAS compliance is rare in practice, including pagination links is a widely adopted and practical application of this principle. Links allow clients to follow the API workflow without constructing URLs manually.

7. Versioning Strategies

APIs evolve over time, and breaking changes are sometimes unavoidable. Versioning your API allows you to introduce improvements without breaking existing clients. There are three common strategies for versioning REST APIs, each with its own trade-offs.

URL path versioning places the version number directly in the URI:

GET /v1/users
GET /v2/users
  • Pros: Highly visible and explicit. Easy to understand, test in a browser, and route at the infrastructure level. Each version can be deployed and maintained independently.
  • Cons: Not technically RESTful because the URI should represent the resource, not the API version. Can lead to code duplication if versions share most of their logic.

Header versioning uses a custom header or the Accept header to specify the version:

GET /users
Accept: application/vnd.myapi.v2+json

# Or with a custom header
GET /users
X-API-Version: 2
  • Pros: Keeps URIs clean and more RESTful. Allows fine-grained versioning. Content negotiation via the Accept header aligns with HTTP standards.
  • Cons: Less visible and harder to test with simple browser requests. Requires clients to set custom headers. Caching becomes more complex.

Query parameter versioning appends the version as a query parameter:

GET /users?version=2
  • Pros: Easy to implement and test. Optional — the server can default to the latest version when the parameter is omitted.
  • Cons: Pollutes the query string, which is semantically meant for filtering. Easy for clients to forget the parameter and inadvertently break when the default version changes.

In practice, URL path versioning is the most widely adopted approach due to its simplicity and clarity. Major APIs from Stripe, GitHub, and Twilio all use some variation of path-based versioning. Regardless of the strategy you choose, document your versioning policy clearly and give clients ample notice before deprecating older versions.

8. Authentication and Authorization

Security is non-negotiable for any public-facing API. Authentication verifies who the caller is; authorization determines what they are allowed to do. REST APIs must implement both, and the choice of mechanism depends on your use case, client types, and security requirements.

API Keys are the simplest form of authentication. A unique key is issued to each client and sent with every request, typically in a header like X-API-Key or Authorization: ApiKey {key}. API keys are easy to implement and suitable for server-to-server communication, but they are not ideal for user-facing applications because they are long-lived and lack fine-grained scoping out of the box.

OAuth 2.0 is the industry-standard protocol for authorization. It allows third-party applications to access user resources without exposing credentials. OAuth 2.0 supports multiple grant types — Authorization Code (for server-side apps), PKCE (for single-page and mobile apps), and Client Credentials (for machine-to-machine communication). While more complex to implement than API keys, OAuth 2.0 provides robust security, token expiration, refresh mechanisms, and scope-based access control.

JSON Web Tokens (JWT) are often used in conjunction with OAuth 2.0 as the format for access tokens. JWTs are self-contained tokens that encode claims (user ID, roles, permissions, expiration time) in a cryptographically signed payload. The server can validate a JWT without making a database query, making them excellent for distributed systems. However, JWTs cannot be revoked before expiration without additional infrastructure like a token blacklist.

# API Key authentication
GET /api/v1/users
X-API-Key: sk_live_abc123def456

# Bearer token (JWT/OAuth)
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Rate limiting is a critical security and reliability measure that restricts the number of requests a client can make within a given time window. Without rate limiting, a single misbehaving client can overwhelm your server and degrade service for everyone. Implement rate limiting using the standard headers:

X-RateLimit-Limit: 1000          # Max requests per window
X-RateLimit-Remaining: 847       # Requests remaining in current window
X-RateLimit-Reset: 1695484800    # Unix timestamp when window resets
Retry-After: 30                  # Seconds to wait (sent with 429 responses)

When a client exceeds the rate limit, return a 429 Too Many Requests status code with a Retry-After header. Consider implementing tiered rate limits based on the client's subscription plan or authentication level.

Best practices for API security include always using HTTPS (never HTTP), validating and sanitizing all inputs, using short-lived tokens with refresh mechanisms, implementing CORS policies for browser-based clients, and logging all authentication events for auditing.

9. Error Handling

How your API handles and communicates errors is one of the most impactful aspects of developer experience. A well-designed error response helps developers quickly identify what went wrong and how to fix it. A poor one leaves them guessing, wastes their time, and leads to frustration and support tickets.

Use a consistent error response format across your entire API. Every error response should follow the same structure so that clients can handle errors programmatically. Here is a recommended error format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body contains invalid fields.",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address.",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "age",
        "message": "Must be a positive integer.",
        "code": "INVALID_TYPE"
      }
    ],
    "requestId": "req_7f3a2b4c",
    "documentation": "https://api.example.com/docs/errors#VALIDATION_ERROR"
  }
}

Key elements of a good error response include:

  • Machine-readable error code: A string constant like VALIDATION_ERROR or RESOURCE_NOT_FOUND that clients can use in conditional logic. Do not rely solely on HTTP status codes for this purpose, as multiple error types may share the same status code.
  • Human-readable message: A clear description of what went wrong. This is for developers reading logs and debugging, not for end users.
  • Validation details: For 400 and 422 errors, include an array of field-level errors so clients know exactly which fields need to be corrected. Each detail should specify the field name, what is wrong, and what is expected.
  • Request ID: A unique identifier for the request that can be used to correlate errors across logs and support conversations.
  • Documentation link: A URL pointing to the relevant documentation for the error, helping developers resolve issues faster.

Never expose internal implementation details such as stack traces, database query errors, or file paths in production error responses. These details are a security risk and provide no value to API consumers. Log them on the server side for debugging and return a sanitized error to the client.

Distinguish between different categories of errors. Validation errors (422) should include field-level detail. Authentication errors (401) should indicate whether authentication is missing or invalid. Permission errors (403) should clarify the required permissions without revealing the existence of resources the user cannot access. Server errors (500) should apologize, include a request ID for support, and assure the user that the issue is being logged and investigated.

10. Documentation and Developer Tools

An API is only as good as its documentation. Even the most beautifully designed API will fail to gain adoption if developers cannot understand how to use it. Comprehensive, accurate, and up-to-date documentation is what separates great APIs from mediocre ones.

OpenAPI (formerly Swagger) is the industry standard for describing REST APIs. An OpenAPI specification is a machine-readable document (in YAML or JSON) that defines every endpoint, parameter, request body, response, and authentication scheme your API supports. From this specification, you can generate interactive documentation, client SDKs, mock servers, and test suites automatically.

openapi: 3.1.0
info:
  title: My API
  version: 2.0.0
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: perPage
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: A paginated list of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'

Essential elements of good API documentation include:

  • Getting started guide: A quick-start tutorial that takes a developer from zero to their first successful API call in under five minutes. Include copy-pasteable code examples.
  • Authentication guide: Clear instructions for obtaining and using API credentials, with examples for every supported authentication method.
  • Endpoint reference: Detailed documentation for every endpoint including the HTTP method, URL, parameters, request body schema, response schema, status codes, and example requests and responses.
  • Code examples: Examples in multiple languages (curl, Python, JavaScript, Go) showing common use cases. Developers should be able to copy an example and modify it for their needs.
  • Error reference: A complete list of error codes with descriptions, causes, and resolution steps.
  • Changelog: A history of API changes, including new features, deprecations, and breaking changes with migration guides.

Postman Collections provide an interactive way for developers to explore your API. Export your endpoints as a Postman collection with pre-configured authentication, example request bodies, and environment variables. This allows developers to test your API without writing any code. Many teams also use Postman collections for automated testing in CI/CD pipelines.

When working with API responses during development, having proper tooling makes a significant difference. Tools like DevTools Pro can help you format and inspect JSON responses from your API, making it easier to verify response structures and debug issues during development.

Consider generating SDKs for popular programming languages from your OpenAPI specification. Official SDKs reduce integration friction, handle authentication and serialization, and provide type safety. Tools like OpenAPI Generator can automate this process so that your SDKs stay in sync with your API as it evolves.

11. Conclusion

Designing a well-structured REST API requires careful thought about a wide range of concerns — from resource naming and HTTP method semantics to authentication, error handling, and documentation. The best practices outlined in this article are not merely academic guidelines; they are battle-tested patterns used by the most successful APIs in the world, from Stripe and GitHub to Twilio and Shopify.

The key takeaways are:

  • Use nouns for resources, plural forms for collections, and HTTP methods for actions.
  • Choose the correct HTTP status code for every scenario — do not default to 200 for everything.
  • Design consistent, predictable request and response formats from day one.
  • Implement filtering, sorting, and pagination for all collection endpoints.
  • Version your API to allow evolution without breaking existing clients.
  • Secure your API with proper authentication, authorization, and rate limiting.
  • Return consistent, detailed, and helpful error responses.
  • Invest in comprehensive documentation — it is your API's most important feature after reliability.

Remember that API design is iterative. Start with a clean, minimal design that follows these principles, gather feedback from real consumers, and evolve thoughtfully. Every decision you make in your API will live far longer than you expect, so take the time to get it right from the beginning. Your future self — and your API consumers — will thank you.

Build Better APIs with the Right Tools

Format JSON responses, debug API payloads, and streamline your development workflow with our free developer tools.

Try DevTools Pro — Free