REST API Design: Best Practices and Lessons Learned

René Kulik on 09.04.2025

Introduction

With over 14 years of experience in designing REST APIs, I have learned quite a bit about what works and what does not. This blog post is a collection of those lessons, distilled into best practices that I have developed over time. It focuses exclusively on RESTful API design, not GraphQL or other paradigms. These are the bread and butter of solid REST API design and represent the fundamentals that every developer should know. Whether you are building APIs for internal applications or public consumption, these principles will help you create a clean, consistent, and maintainable API.

HTTP Request Methods

A well-designed API makes proper use of HTTP methods. Here is a breakdown of each method and how it should be used:

Methods that are designed to transport data, such as POST, PUT, and PATCH, should use the request body to send data instead of relying on query parameters. Query parameters are typically reserved for filtering or specifying resources in the URL, while the body is the proper place for submitting data, especially when dealing with complex or large payloads. Using the body ensures better organization, clarity, and adherence to HTTP semantics.

While GET technically supports request bodie, this is not considered best practice. GET is designed to be a safe, idempotent method primarily used for retrieving resources. Including a body with a GET request is unconventional and can lead to confusion about the request’s intent. Additionally, GET requests are often cached, and including a body could break caching mechanisms, leading to inconsistent results or performance issues. For this reason, it is best to pass parameters via the URL (i.e., query parameters) in GET requests.

URL Structure and Naming Conventions

When designing URLs, follow conventions that enhance clarity and scalability. The following examples are based on a blog system. Here are a few practical tips to keep in mind:

  1. Ensure each URL uniquely identifies a resource – A well-structured REST API uses URLs that clearly and uniquely reference a specific resource or collection. For example, GET /posts/1 should return to the post with ID 1, whereas GET /posts refers to the entire collection of posts. This improves clarity, consistency, and enables more predictable caching.
  2. Reflect nested resources in the route – This makes the parent-child relationship explicit. For example, instead of using POST /comments, prefer POST /posts/:id/comments to make it clear that the comment belongs to a specific post.
  3. Isolate properties that should be edited independently – If a certain property of a resource is commonly updated on its own, consider making it a sub-resource. For example, to update only the title of a post, use PUT /posts/:id/title. Since you are replacing the entire title, PUT is the appropriate HTTP method here.
  4. Treat different states of a resource as separate resources – State transitions can be modeled with separate endpoints. For instance, POST /published-posts is used to publish a post. Although this is an update action and PATCH /posts/:id would also work, it is cleaner and more expressive to give this new state (a “published” post) its own resource and URL. To unpublish a post, use DELETE /published-posts/:id.
  5. Use pluralized endpoints – Pluralized endpoints make sense because they represent collections of resources, ensuring consistency across the API.

Posts

Comments

Response Structure

Consistency in API responses makes integration easier for frontend developers. Here are some best practices for API responses:

Data Types

It is important to ensure that the types in your API responses match the expected data types. For example, if a value is a number in your system, it should be returned as a number in the response, not as a string. This helps maintain consistency and prevents type mismatches during integration.

For dates, always use the ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). This format is widely accepted and helps avoid confusion, as it clearly represents both the date and the time, along with time zone information. For example:

{
  "data": {
    "id": 1,
    "title": "Third Post",
    "content": "This is the content of the third post.",
    "createdAt": "2025-04-09T13:00:00Z",
    "updatedAt": null,
    "publishedAt": "2025-04-09T14:00:00Z"
  }
}

In JavaScript, you can easily generate this ISO 8601 format with new Date().toISOString().

Using the correct types and formats ensures clarity, better interoperability, and a more predictable API.

Single Resource Response

This is an example of a response when retrieving a single resource. The resource is wrapped in a data object.

{
  "data": {
    "id": 1,
    "title": "First Post",
    "content": "This is the content of the first post.",
    "createdAt": "2025-04-08T13:00:00Z",
    "updatedAt": null,
    "publishedAt": null
 }
}

Multiple Resources Response with Pagination

This is an example of a response when retrieving multiple resources. The resources are wrapped in a data array, and additional metadata, such as pagination info, is included under the meta object.

{
  "data": [
    {
      "id": 1,
      "title": "First Post",
      "content": "This is the content of the first post.",
      "createdAt": "2025-04-08T13:00:00Z",
      "updatedAt": null,
      "publishedAt": null
    },
    {
      "id": 2,
      "title": "Second Post",
      "content": "This is the content of the second post.",
      "createdAt": "2025-04-09T12:34:56Z",
      "updatedAt": null,
      "publishedAt": null
    }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 2,
    "totalPages": 5,
    "totalItems": 10
  }
}

Communication Between Backend and Frontend Teams

One of the biggest API design pitfalls happens when backend and frontend teams communicate in example requests and responses rather than schemas. Define clear JSON schemas that outline the exact structure of requests and responses. This eliminates ambiguity and ensures smooth collaboration.

Tools like Swagger can be extremely helpful in this regard. Swagger allows both backend and frontend teams to work from a common definition, providing an interactive documentation interface. It ensures that both teams are on the same page regarding the structure of the API and makes it easy to update documentation as the API evolves. Swagger even allows automatic generation of client libraries and server stubs, further simplifying the development process.

Conclusion

Designing a well-structured REST API requires attention to detail, adherence to best practices, and clear communication between teams. By following these principles, you can build APIs that are intuitive, maintainable, and easy to work with. Whether you are a seasoned developer or just starting, these guidelines will help you create APIs that stand the test of time.