- Introduction
- HTTP Request Methods
- URL Structure and Naming Conventions
- Response Structure
- Communication Between Backend and Frontend Teams
- Conclusion
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:
- GET: Used to retrieve resources.
GET
requests are designed to be safe and idempotent. A successfulGET
request usually returns a200 OK
status code. - POST: Used to create new resources. This method is not idempotent, meaning multiple identical requests could create multiple resources. A successful
POST
request typically returns a201 Created
status code along with the newly created resource. - PUT: Used to fully replace a resource. This method expects the complete resource representation, and anything missing from the request might be deleted. Even if certain fields (like
id
) are not meant to be updated, it is still common practice to include the entire resource object in aPUT
request, including those unchangeable fields. A successfulPUT
request returns a201 Created
status code if a new resource is created, and a200 OK
status code if an existing resource is updated, along with the created/updated resource in the response. - PATCH: Used for partial updates. Unlike
PUT
,PATCH
allows updating only specific fields without sending the entire resource. A successful PATCH request should return a200 OK
status code along with the updated resource. - DELETE: Used to remove a resource. If the deletion is successful, the response should reflect that by returning a
204 No Content
status code.
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:
- 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, whereasGET /posts
refers to the entire collection of posts. This improves clarity, consistency, and enables more predictable caching. - Reflect nested resources in the route – This makes the parent-child relationship explicit. For example, instead of using
POST /comments
, preferPOST /posts/:id/comments
to make it clear that the comment belongs to a specific post. - 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. - 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 andPATCH /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, useDELETE /published-posts/:id
. - Use pluralized endpoints – Pluralized endpoints make sense because they represent collections of resources, ensuring consistency across the API.
Posts
POST /posts
– Create a postGET /posts
– Retrieve all postsGET /posts/:id
– Retrieve a specific post by IDPATCH /posts/:id
– Update a specific postPUT /posts/:id/title
– Update the title of a post (Isolated property that can be edited independently)DELETE /posts/:id
– Delete a specific postPOST /published-posts
– Publish a post (Different state === separate resource)DELETE /published-posts/:id
– Unpublish a post
Comments
POST /posts/:id/comments
– Create a comment (This is a nested resource belonging to the post, so the URL reflects that relationship)GET /posts/:id/comments
– Retrieve all comments for a postGET /comments/:id
– Retrieve a specific commentPATCH /comments/:id
– Update a commentDELETE /comments/:id
– Delete a comment
Response Structure
Consistency in API responses makes integration easier for frontend developers. Here are some best practices for API responses:
- Always return the full resource representation, including
null
values.- Even
null
values provide meaningful information. For example, apublishedAt
field beingnull
signals that a post has not been published, and averifiedAt
field beingnull
indicates that a user has not verified their email yet.
- Even
- Avoid redundant prefixes in property names. Instead of using
postId
, simply useid
when inside apost
resource. - Ensure that the data types in your API responses match their expected types, such as using numbers for numerical values and strings for text, to maintain consistency and avoid errors.
- Use a standardized response schema. A common convention is wrapping responses in a
data
object, as this allows easy extension with additional information, such as ameta
object for pagination. - I prefer using camelCase for property names because it aligns with the way most of my application code is written. This consistency makes it easier to map data between backend and frontend without any transformation. It also improves readability and reduces friction during integration, especially in JavaScript-heavy environments.
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.