In the first article of this series we looked into the naming conventions used in REST API's. Using a fixed naming convention thats fits the audience is a great starter for your API. But more is needed in order to build an API that is easy to use and maintain.
The sum of the elements we will look at over this series is this:
But for now, let's get started looking at recognizable design patterns for your REST API.
The most used generic pattern for a REST API is the CRUD repository pattern: Create, Read, Update, Delete.
You will find the CRUD repository pattern in every corner of software development and very much in relation to database persistency. When it comes to REST API's the pattern also elegantly fits here, although it is typical to add List to the array of actions (which is sometimes referred to as CRUDL).
Check out the following overview that shows how each action of a CRUD pattern is mapped to an HTTP method on an API serving Cats.
Action | Http Method | Example |
---|---|---|
Create | POST | POST https://myserver/cats |
Read | GET | GET https://myserver/cats/{id} |
Update | PUT | PUT https://myserver/cats/{id} |
Delete | DELETE | DELETE https://myserver/cats/{id} |
List | GET | GET https://myserver/cats |
This will cover all the basic needs of an API. You will be able to both persist as well as retrieve entities. In later articles we will even look into how you can do paging, sorting and searching on the List endpoint. For now, lets just look at some simple examples for the different requests. For the sake of the example the model we use describes a Cat and looks like this:
{
id?: string,
name: string,
age: number
}
For each of the following requests will be an example of how create the object and send it using the Fetch Api. The HTTP envelopes will also be described in a compact manner.
The first thing you may need to with a REST Api is to create some data to work with. So with reference to the model mentioned, we prepare an object that contains the following:
{
"name": "Garfield",
"age": 45
}
We leave out the id in this case. It is optional in the model because we expect the backend to create the id for us (Actually, if we are using globally unique ids then we could create the id's on the client as well, but that is a whole other story). Now, we want to create the data so check out the example below.
const cat = {
name: "Garfield",
age: 45
};
fetch('https://myserver/cats', {method: 'POST', body: cat})
.then((response) => response.json())
.then((cat) => console.log(cat));
POST https://myserver/cats
content-type: application/json
{
"name": "Garfield",
"age": 45
}
HTTP/1.1 200
content-type: application/json
{
"id": "cat-1",
"name": "Garfield",
"age": 45
}
Notice how we receive the persisted object in return, with the id generated by the backend.
Reading an object from the backend is not much different. Now that we have already persisted a Cat and it was given the id cat-1
we will use that id when retrieving it.
fetch('https://myserver/cats/cat-1')
.then((response) => response.json())
.then((cat) => console.log(cat));
GET https://myserver/cats/cat-1
HTTP/1.1 200
content-type: application/json
{
"id": "cat-1",
"name": "Garfield",
"age": 45
}
Whenever we need to update the data for the Cat, we can send an update request. The id in the update request is still optional. The id is part of the Url, and the backend should be able to handle the fact the that id is left out of the model when updating it, as the Url already specifies the id. In the following example, we update the age of Garfield from 45 to 46.
const cat = {
name: "Garfield",
age: 46
};
fetch('https://myserver/cats/cat-1', {method: 'PUT', body: cat})
.then((response) => response.json())
.then((cat) => console.log(cat));
POST https://myserver/cats/cat-1
content-type: application/json
{
"name": "Garfield",
"age": 46
}
HTTP/1.1 200
content-type: application/json
{
"id": "cat-1",
"name": "Garfield",
"age": 46
}
Notice again, what we get back in the response is the persisted version with updated data for the entity in the backend.
The delete request if probably the most simple request of the all. We do not send any data to the server and we do not expect anything in return. We simply send a request and expect the data to be deleted.
fetch('https://myserver/cats/cat-1', {method: 'DELETE'})
.then((response) => /* Do something with the response */);
DELETE https://myserver/cats/cat-1
HTTP/1.1 200
The final request we will cover is the list request. As mentioned above, we will cover searching, paging and sorting in a later article. For now we will just cover the basics of the list request. For the sake of the example, let's imagine we have already created 2 Cats: Garfield and Tom. Lets create a request that returns both of them.
fetch('https://myserver/cats')
.then((response) => response.json())
.then((cats) => console.log(cats));
GET https://myserver/cats
HTTP/1.1 200
content-type: application/json
[
{
"id": "cat-1",
"name": "Garfield",
"age": 45
},
{
"id": "cat-2",
"name": "Tom",
"age": 83
},
]
In other words, what we get in return for a single request, is not a single Cat, but an array of Cats. Actually, all the Cats we have created. This is fine when we only have 2, but as the dataset grows we really do need paging.
But what about the things that do not fit into a CRUD repository pattern? What if we need to make a request that potentially effects all the Cat entities of the API - like for example Feed all the Cats. Using the CRUD repository pattern we would need to update each Cat which could be an unlimited number of requests (Cats tend to multiply). For those situations we may need to introduce an Action pattern that lays outside of the CRUD pattern, as described in the next section.
As you build your REST Api you may start to notice that not everything fits into a CRUD pattern. Let's say we need a way to feed all the cats in our Api. It is a crucial thing to do, but is not something that is exposed in the model so we cannot update a cat entity to feed it. Further more, if we had millions of cats created, we would need to make millions of requests to update them all – or at least create a special endpoint to feed them all.
Instead of creating special endpoints to handle these situations we create a pattern for it. More specifically an Action pattern which is merely a convention of where to place these actions in your api.
The convention is that every endpoint can expose actions to use. Therefore we reserve the word actions
in our path structure so that we – by convention – always can add actions.
Action | Http Method | Example |
---|---|---|
Feed all Cats | POST | POST https://myserver/cats/actions/feed |
Feed one Cat | POST | GET https://myserver/cats/{id}/actions/feed |
It is important to follow recognizable design patterns for your REST API. It makes it easy to comprehend and trivial to create clients for it. But there is still more you can do to ensure the quality of your API.
Follow along in this series as we cover multiple aspects of how to craft a REST API that is easy to expand, document, and use. In the next article, we will go into depth with Idempotency and why you need to consider it when developing a quality REST Api.