REST architecture constraints
Possibly one of the main benefits of REST is the architectural style that it defines via what are known as the REST architecture constraints. These guiding principles allow software designers to have a common ground for beginning their API design whose advantages and disadvantages are already known, tried, and tested. There are six of these constraints:
- Client-server
- Uniform interface
- Statelessness
- Caching
- Layered
- Code on demand
One important note before we review these is that these aren’t a hard constraint on how you should build your software. These should be interpreted more like guidelines instead of hard rules because no single architecture is the right design for every case.
REST architecture constraints
Client server
This is the first and possibly most basic REST constraint: REST applications should have a client-server architecture. In this context, the client is the entity that requests the resource and the server is the entity that provides the resource as a response to the client’s request. Note than a server isn’t restricted to serving a single resource nor to serving a single client.
Under this architecture, the client and the server should not run in the same process space. This means that we get the following advantages out of this:
- Separation of concerns - we have the client, which is the value provider to the end consumer and the server, which is the API provider for the client. Remember folks, healthy boundaries help make a relationship healthier;
- Independent evolution - since we have both components running in separate process spaces, we can updated them independently.
Note that the REST application will continue to work as long as there is a uniform interface between the client and the server, which is the perfect introduction for the next REST architecture constraint.
Uniform interface
The principle is simple: we want the client and the server to share a common technical interface, i.e., a contract established between the client and the server via which they communicate. This contract is usually created by following a few simple principles:
- Individual resources are identified in the request, e.g.,
GET /pokedex/1
should return the first Pokémon in the Pokédex; - Representation of resources:
- The API client should receive the representation of the resource, i.e, the information about the requested resource;
- The representation of the resource can be used to carry out manipulation of the resource, which means that you can use the ID to patch or update the internal resource representation by doing another request;
- How the data is represented in the server side does not need to match (and often doesn’t) the format that is most convenient for the client;
- Ideally, messages should be self-descriptive, in the sense that they should contain metadata such as the content type, HTTP status code, etc;
- And, in some instances, the server does not have to be limitted to returning only the requested resource: it can also send back hypermedia, i.e., links, that can be used on the client side for discovery.
Statelessness
Formally put, this means that the client should manage its own state instead of relying on the server to do so. This translates simply to the server having no “memory” of what requests have been performed in the past. If such history management is required, it should be handled by the client. This greatly simplifies server design, makes it far easier to scale the server side horizontally and also reduces the amount of resources required by the server.
For the server, each request receifed from the client applications are self-contained, i.e., if there is any state information required, it is provided by the client in its request. From a practical standpoint, if we take into account that a single server handles requests from multiple clients, it would be highly impractical to have it remember the request history, i.e., state, of each and every one of these clients. But statelessness also has it drawbacks, which is where the next architecture contraint comes in.
Caching
Let’s think about what challenged statelessness introduces. Since the server has no state, this has to be sent by the client, which means that the request data size is larger. Additionally, clients require multiple calls to get the resource representation they desire. When we combine these two factors, we need a server that can handle larger and more numerous requests, which might entail some performance degradation.
To address these limitations of statelessness, we introduce the concept of caching! Caching is just another layer on our architecture that stores a subset of our data which helps tackle scalability and performance issues.
To better understand caching, it helps to introduce different types of caching, which mostly just relate to where this caching layer lives:
- Database caching, where the caching layer typically exists in the database itself and serves the purpose of avoiding to read disk information directly for common requests;
- Local server cache, where the caching layer exists in the server itself storing common request/response pairs and avoiding database calls (which are typically more expensive);
- Client-side caching, where the client itself is responsible for caching data, which means that common requests that are repeated are very fast, since they do not even require sending an HTTP request to a server;
- Proxy or load balancer level caching, where a new layer between the client and the server is introduced to handle caching (among other things).
One neat thing about HTTP requests is that cache-control directives exist and are HTTP headers that allow us to control how caching works on applications and its intermediaries. For example, we might want to not cache sensitive information, such as credit card numbers.
Layered
Layering of software architecture is not restricted to when we are designing a RESTful application, but an overall useful concept for effective system design. The concept is very straightforward: each layer has a unidirectional dependency on the layer next to it and each layer can only connect with the layer that it depends on (and the one that depends on it, obviously). This means, for example, that the client always has to go through the server to get information from the database, it can never connect directly to it (and never should, please do not do this, it is a huge security risk). Layering simplifies the system’s architecture by reducing dependencies and allowing different parts of the system to scale independently. Additionally, no software architecture is static, it evolves over time as new features are developed and the user base grows. And layering also helps with this, since a change to a layer impacts, at most, only one layer, making it easier to debug issues and reduce blast radius.
Code on demand
The last REST architecture constraint is also an optional constraint called code on demand. Under this constraint, the server has the ability to extend the client’s functionality by sending it code. Practically, this means that servers can send scripts/applets to clients for them to run, adding features and thus reducing the complexity of a client. It is very rare for REST APIs to actually implement such functionality and this seems to be mostly an artefact of the past, since nowadays web apps are very popular and used by many of the largest software players.