REST APIs don’t actually use HTTP verbs to do things, they use them to describe things.
Let’s see what that means in practice. Imagine you want to fetch a list of users. You’d send a GET request to /users. The server, if it’s following REST principles, isn’t executing a "get users" command. Instead, it’s interpreting GET as an instruction to represent the collection of resources identified by /users. The response you get back, usually JSON, is that representation. If you wanted to create a new user, you’d send a POST request to /users with the user’s data in the body. Again, POST isn’t an action; it’s a description of the operation: "create a new resource within the /users collection."
This distinction is crucial for building scalable and maintainable APIs. RESTful APIs are designed around resources, which are simply any objects or data that can be named. Think of them as nouns. A user, a product, an order – these are all resources. The API then exposes endpoints (URLs) that represent these resources or collections of resources.
Here’s a typical interaction:
1. Fetching a specific resource:
GET /users/123
This requests a representation of the user resource with the ID 123.
2. Creating a new resource:
POST /users
Host: api.example.com
Content-Type: application/json
{
"name": "Alice Smith",
"email": "alice.smith@example.com"
}
This sends data to create a new user within the /users collection. The server might respond with 201 Created and a Location header pointing to the new resource’s URL, e.g., /users/456.
3. Updating an existing resource:
PUT /users/123
Host: api.example.com
Content-Type: application/json
{
"name": "Alice Jones",
"email": "alice.jones@example.com"
}
PUT is generally used for complete replacement of a resource. You send the entire new state.
4. Partially updating an existing resource:
PATCH /users/123
Host: api.example.com
Content-Type: application/json
{
"email": "alice.j@example.com"
}
PATCH is for applying partial modifications.
5. Deleting a resource:
DELETE /users/123
This requests the removal of the user resource with ID 123.
The power of this approach lies in its simplicity and predictability. Clients interact with resources by using standard HTTP methods, and the server responds with standard HTTP status codes and representations of those resources. This makes it easier for developers to understand and use your API, as it aligns with established web standards.
Now, let’s talk about responses. A well-designed REST API provides meaningful responses. This includes using appropriate HTTP status codes. Instead of just returning 200 OK for everything and indicating errors in the response body, you should use codes that convey the outcome:
200 OK: Standard response for successful GET, PUT, PATCH.201 Created: Resource successfully created (usually after POST).204 No Content: Successful request but no content to return (e.g., after DELETE).400 Bad Request: Client error, e.g., invalid JSON, missing parameters.401 Unauthorized: Authentication required.403 Forbidden: Authenticated, but lacks permission.404 Not Found: Resource doesn’t exist.500 Internal Server Error: Server-side problem.
Beyond status codes, the response body should be consistent. For a successful GET /users/123, you might get:
{
"id": 123,
"name": "Alice Jones",
"email": "alice.j@example.com",
"createdAt": "2023-10-27T10:00:00Z"
}
For an error, like 404 Not Found:
{
"error": {
"code": "resource_not_found",
"message": "User with ID 123 not found."
}
}
This structured error response allows clients to programmatically handle different types of API errors.
Versioning is another critical aspect. APIs evolve. You’ll need to add new fields, change existing ones, or even deprecate endpoints. Versioning allows you to introduce these changes without breaking existing clients. The most common strategies involve:
-
URL Versioning: Appending a version number to the URL.
GET /v1/users/123GET /v2/users/123This is the most explicit and easiest to understand.
-
Header Versioning: Using a custom HTTP header.
GET /users/123 Host: api.example.com Accept-Version: v1Or using the
Acceptheader:GET /users/123 Host: api.example.com Accept: application/vnd.example.v1+jsonThis keeps your URLs cleaner but can be less obvious to discover.
-
Query Parameter Versioning: Less common and generally discouraged for REST, but possible.
GET /users/123?version=1
For most scenarios, URL versioning is the clearest path forward. When you introduce v2, v1 remains untouched, allowing older clients to continue functioning while new clients can adopt the updated API.
The one detail that trips many people up is that a GET request to /users should ideally return a list of user objects, not just an array of arbitrary data. Each object in the list should conform to the structure of a single user resource, providing a consistent representation whether you’re fetching one user or many. This means the response for GET /users might look like:
{
"data": [
{
"id": 123,
"name": "Alice Jones",
"email": "alice.j@example.com"
},
{
"id": 124,
"name": "Bob Williams",
"email": "bob.w@example.com"
}
]
}
Notice the top-level data key. This allows for future expansion, like adding pagination metadata:
{
"data": [ ... ],
"pagination": {
"totalItems": 50,
"currentPage": 1,
"pageSize": 10,
"nextPage": "/users?page=2&size=10"
}
}
Understanding how to properly structure resources, use HTTP status codes effectively, and implement a robust versioning strategy is foundational to building APIs that are both powerful and easy to work with.
The next challenge you’ll face is handling complex filtering and sorting of these resources.