Designing a REST API: Recognizable design patterns

Introduction

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:

  1. Naming conventions
  2. Recognizable design patterns (This article)
  3. Idempotency (Future article)
  4. Paging and sorting (Future article)
  5. Searching (Future article)
  6. Patching (Future article)
  7. Versioning (Future article)
  8. Authentication (Future article)
  9. Documentation (Future article)

But for now, let's get started looking at recognizable design patterns for your REST API.

Repository Pattern

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.

ActionHttp MethodExample
CreatePOSTPOST https://myserver/cats
ReadGETGET https://myserver/cats/{id}
UpdatePUTPUT https://myserver/cats/{id}
DeleteDELETEDELETE https://myserver/cats/{id}
ListGETGET 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.

Create Request

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.

Javascript & Fetch

const cat = {
    name: "Garfield",
    age: 45
};

fetch('https://myserver/cats', {method: 'POST', body: cat})
  .then((response) => response.json())
  .then((cat) => console.log(cat));

Http Request

POST https://myserver/cats
content-type: application/json

{
    "name": "Garfield",
    "age": 45
}

Http Response

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.

Read Request

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.

Javascript & Fetch

fetch('https://myserver/cats/cat-1')
  .then((response) => response.json())
  .then((cat) => console.log(cat));

Http Request

GET https://myserver/cats/cat-1

Http Response

HTTP/1.1 200
content-type: application/json

{
    "id": "cat-1",
    "name": "Garfield",
    "age": 45
}

Update Request

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.

Javascript & Fetch

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));

Http Request

POST https://myserver/cats/cat-1
content-type: application/json

{
    "name": "Garfield",
    "age": 46
}

Http Response

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.

Delete Request

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.

Javascript & Fetch

fetch('https://myserver/cats/cat-1', {method: 'DELETE'})
  .then((response) => /* Do something with the response */);

Http Request

DELETE https://myserver/cats/cat-1

Http Response

HTTP/1.1 200

List Request

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.

Javascript & Fetch

fetch('https://myserver/cats')
  .then((response) => response.json())
  .then((cats) => console.log(cats));

Http Request

GET https://myserver/cats

Http Response

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.

Actions

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.

Every endpoint has actions

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.

ActionHttp MethodExample
Feed all CatsPOSTPOST https://myserver/cats/actions/feed
Feed one CatPOSTGET https://myserver/cats/{id}/actions/feed

Idempotency

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.