- Published on
Rest API - Best Practices - Design
- Authors
- Name
- Lucian Oprea
- @LucianDSA_
Introduction
In 2000, Roy Fielding introduced the REST model, a new way to design web services.
This model has become the industry standard for building modern web applications and services. RESTful web services are now ubiquitous and the go-to solution for creating web-based APIs.
It is used by organizations of all sizes and across various industries. Therefore, understanding how to properly design a REST API is a crucial skill for software developers.
There are different stages of maturity in designing a REST API.
Level 3 is the highest level, indicating a truly RESTful API according to Fielding's definition. However, in practice, many published web APIs are only at level 1 or 2.
Reaching level 2 requires practice, but the effort is worth it if you want to build REST APIs that are high-quality, reliable, and scalable. It's difficult to reach level 3 in the real world and we'll explore the reasons why.
Stateless API đ§
A REST API should be Restful and Stateless, not Restless and Stateful.
What does Stateless actually mean?
In a distributed environment, stateless means that a client request is not bound to a specific server. So, the servers donât maintain any session state with the clients. Therefore, the client is free to interact with any server in a load-balanced fashion, without being tied to a specific server.
In a single server environment, stateless means that a server can process the request, without any knowledge of previous requests from a certain client.
Why is this important? This makes the API more scalable
, as requests can be processed by any available server, without relying on specific state from the server.
Also, this makes the API more available
. If a web server fails, then incoming requests can be routed to another instance, while the failed server is restarted, with no bad effects on client applications.
There are advantages, even on a single instance environment. For instance, a stateless API can be more easily cached and optimized. This is because the same response can be returned for identical requests, without needing to store any state information on the server. This can improve performance
and reduce server load, even if only a single server is being used.
Also, since there is no need to set up complex session state or context information, which can make the API more modular and easier to maintain or test. Now, the server can be tested in isolation
.
Making Stateful Apps Stateless
Advantages of a Stateless API are clear, but sometimes we need to store state to make progress with requests and get work done.
To ensure the correct items and prices are being calculated, the server must maintain state information for a shopping cart on an e-commerce website.
The client can add, remove, or modify items as they navigate the site. To make the app stateless, follow these steps:
- Identify the state of the app. In this case we have the items names, quantities and prices.
- Instead of storing the state within the app, we need to store the state externally, for example in a database or in a cache. This ensures that the app doesnât rely on any internal state, and it can operate independently.
- The client would include a session ID or cookie, in subsequent requests to ensure that the server is accessing the correct cart.
This ensures that any interaction with the app can be identified, and the state can be retrieved accordingly, from any server and without prior informations
.
This approach involves keeping some state information on the back-end, but the API is still intended to be stateless. This allows us to take advantage of its benefits.
Therefore, a complete definition is:
A stateless REST API is when each client request includes all the required information to process the request, and the server does not maintain any session state or context information between requests.
Organize the API design around resources
A good API design is organized around resources, for example, customers or orders and not actions or verbs. For instance, endpoints like âcreate-orderâ should be avoided.
https://e-commerce.com/orders // Good
https://e-commerce.com/generate-order // Avoid
https://e-commerce.com/users // Good
https://e-commerce.com/create-user // Avoid
Why is this poor design?
Because we have the HTTP protocol that brings the action. We have the HTTP methods methods or verbs: GET, POST, PUT, PATCH and DELETE to handle the actions.
This way, we provide consistency
between different endpoints.
And when we have consistency, clients can make assumptions about the behavior of the API based on their prior knowledge of the HTTP protocol.
Moreover, at a high-level, HTTP verbs map to CRUD operations of a database:
HTTP | Databse |
---|---|
GET | Read |
POST | Create |
PUT | Update |
DELETE | Delete |
Real world Example
While the previous examples are straightforward, real-world scenarios can be more complex.
For example, consider how to model an endpoint that returns customer orders with the ability to sort by an attribute and paginate the results.
We have a 1 to many relationship here, and we can represent it with path parameters.
GET /customer/orders
However, we can improve this API.
Tip 1: Entities are grouped into collections.
Usually, entities are grouped into collections, such as orders and customers. Basically, we organize resources hierarchically, which makes the API more intuitive
.
In general, it's helpful to use plural nouns for URIs that reference collections. This provides consistent naming convention
when we need to get all customers or only a particular one.
GET /customers/customer/orders
Tip 2: Use parametrized URIs for identity. To identify a specific user, we use parameterized URIs. Generally, path parameters are recommended when you need to specify the identity or key of a specific resource being accessed or modified, and not query parameters.
GET /customers/{customer_id}/orders
Tip 3: Avoid resource URIS more complex than /collection/item/collection Avoid resource URIs that are more complex than 2 levels
. For example, instead of having customers/orders/products, which has 3 levels, we could have 2 simpler URIs that serve the same purpose.
/customers/1/orders/99/products // Avoid
/customers/1/orders // Good
/orders/99/products // Good
This simplifies maintenance and allows for greater flexibility in the future.
Tip 4: Use query params for additional options or metadata.
To sort the collection, we can use query parameters to provide additional options or metadata. For example, we can sort by price.
GET /customers/{customer_id}/orders?sort=price&limit=10
Query parameters are generally recommended for filtering, sorting, and pagination or when additional properties or options need to be passed to an operation.
In this case, the sort query parameter sorts the orders of a customer, and the limit parameter specifies that only the first 10 matching results should be returned.
To summarize, we used customer as a resource, orders as a subresource, and query parameters to get further options on those resources.
Do not return plain text đ
Returning plain text for a REST endpoint is not a good idea. Why?
When a client application receives plain text instead of a structured media type, it needs to perform additional parsing and processing to extract the necessary data. This can introduce errors and inefficiencies that are undesirable.
It is ideal to use JSON, XML or YAML to represent and transmit data. These media types provide a structured way of representing data, making it easy for the client application to parse and understand the data being returned.
For REST APIs, JSON is the preferred option if possible. It is widely supported in modern programming languages and frameworks, more so than XML or YAML. XML is also well-supported, but it has unnecessary verbosity. Using XML can result in larger file sizes, slower parsing, and increased bandwidth usage.
YAML is less verbose and more expressive than JSON. However, it does not have the same level of compatibility across programming languages and systems as JSON.
Versioning a RESTful web API
What happens when we change the API? đ§¨
Changing a REST API after it's been adopted by clients is one of the worst things to do.
Clients suddenly find out the hard way that the API they've been using is not working anymore.
This leads to broken code, failing applications and angry users.
Clients trust your API to remain stable and predictable, and changing it betrays that trust.
This forces clients to update documentation, modify code and provide support to their clients.
This should be done ASAP to ensure that clients continue to work as before.
In short, changing a REST API is a recipe for disaster, unless you have a very good reason for doing so and a solid plan for managing the transition.
How to handle API updates?
The common way to update a web API is by versioning.
We can specify the version of the API, in the URI, by appending query parameters, by adding HTTP headers, or by specific Media Types.
Which one should we chooseâ
Letâs see the benefits and trade-offs.
Considering performance implication, the URI versioning and Query String versioning schemes are cache-friendly.
These 2 approaches are pretty common, however from a Restful perspective the URL should not be different depending on the version when fetching the same data.
So, for Rest purists, we have the options to specify the version, using a Custom or an Accept header. These are less intrusive, since they donât change the URL.
Development team preferences should dictate the choice of API versioning, with URI versioning being the simplest and Media Type considered the purest.
API Versioning | Example |
---|---|
URI | domain.com/v2/customers |
Query Params | domain.com/customers?version=2 |
Headers | domain.com/customers Custom-Header: api-version=2 |
Media Type | domain.com/customers Accept: application/e-commerce.v1+json |
Handling Exceptions
When developing a Rest API, it's important to have solid exception handling to prevent uncaught exceptions that could propagate to the client.
For instance, if a user requests user details, the API might require a user ID as a number.
Instead of a generic error message and status code of 500, we should catch the exception and wrap it with a descriptive message and appropriate HTTP status code.
Using the right status code
is crucial, and you can refer to the Status code definitions page published by the standards organization IETF for guidance. https://www.rfc-editor.org/rfc/rfc9110.html#name-status-codes
Distinguish between client-side errors
that require client changes and server-side errors
that the application must address. Monitoring 5xx errors helps identify server problems.
A global error handling strategy
improves the user experience by providing clear and consistent error messages, and makes the API scalable while reducing the amount of duplicate code needed to handle errors.
Worth to HATEOAS?
The last level of the REST API maturity is to use hypermedia or HATEOAS.
This is achieved through links in the representation of an order that identify the available operations on that order.
{
"orderID": 3,
"productID": 2,
"quantity": 4,
"orderValue": 16.60,
"links": [
{
"rel": "customer",
"href": "https://adventure-works.com/customers/3",
"action": "GET",
"types": [
"text/xml",
"application/json"
]
},
{
"rel": "self",
"href": "https://adventure-works.com/orders/3",
"action": "DELETE",
"types": []
}
]
}
This provides a discoverable and self-descriptive API, which allows the server to change URIs without breaking clients. However, this principle has some major disadvantages.
Performance Concerns:
- Including links in API responses can impact performance, especially for APIs with many requests for the same resource. In such cases, including links is a waste of resources.
Lack of Standardization:
- There is no widely accepted standard for implementing HATEOAS in REST APIs.
Low Adoption:
- Due to reasons such as limited client support and complexity, HATEOAS remains more of a theoretical principle and is not commonly put into practice.