REST API and service architecture

Hidden text

One day, while discussing the review code with a colleague, I noticed some subtleties of the REST API, which affect not only the ease of use and support of the API, but also have a direct impact on the stability and scalability of the service.

Introduction

Good day!

I would like to invite our readers to talk a little about such an unfashionable topic as the REST API. Without delving into the differences between a REST API and a REST-like API, everyone has dealt with such a thing: from junior to senior. Moreover, when going through an interview, you can hear a completely natural question: what is a REST API? If you don’t think too much, but just remember what you were dealing with when designing the API of the next service, you can answer: well, it uses http 1.1. There are also verbs GET, PUT, POST, DELETE, PATCH and some more. Each verb is responsible for one thing: creating, changing, deleting, reading, etc. The error codes used in the response are: 200, 301, 401, 404, 500, etc. Each code means that everything was successful, or some problems arose: no rights, page not found, internal server error, etc. And in principle, such an answer cannot be called incorrect. That's right… but it's superficial – it doesn't reflect the essence of the REST API. Without understanding the essence, it is difficult to avoid making mistakes when designing. And to understand, you need to understand the REST API, that is, give the correct definition.

Definition

So, what is a REST API? Representational State Ttransfer APplication Ininterface – application interface for transferring representative state. Many have heard this formulation. It is quite difficult to understand. I would like to offer a simpler definition: REST API is about entities and their states. Although, perhaps, it has not become clearer 🙂 I will try to convey my idea with specific examples. Let's assume we need to create a user entity named “Ostap Bender”:

POST /users

{
  "name": "Остап Бендер",
  "age": 42,
  "email": "bender@mail.ru"
}

POST request /users will create an entity with the required field values, that is, with the required state. If you need to get the user entity, then this is done with the request:

GET /users/1

Possible answer: 200th error code and response body

{
  "name": "Остап Бендер",
  "age": 42,
  "email": "bender@mail.ru"
}

And if you are interested in an entity that belongs to the user entity, for example email, then the request may look like this:

GET /users/contacts?type=email

In both examples we are talking about the user entity. The first demonstrates creating an entity with a certain state, and the latter demonstrates reading the state of the entity. I agree that the examples discussed can hardly convince that the REST API is about entities and their states, because A user can be created with the following request:

POST /user/create

{
  "name": "Остап Бендер",
  "age": 42,
  "email": "bender@mail.ru"
}

What is prohibited? Everything will work. And I once did this. In url /user/create We see the verb create – the API turns out not only about entities, but also about actions on entities. But such a handle will seem correct if you do not know about the recommendations for the REST API.

REST API Recommendations

In addition to the correct choice of verbs (POST, GET, etc.), return error codes, you should also adhere to some recommendations regarding url handles: 1) use plural nouns, 2) indicate the entity identifier if we are talking about a specific entity, 3 ) there shouldn’t be any verbs… Yes, I know, usually recommendations start to interest you when you “hit the bumps”, unless, of course, you draw the conclusion: “this is a feature of the technology, you shouldn’t expect anything more from it.” Nevertheless, I would like to show what mistakes the recommendations allow you to avoid. Therefore, let’s return to the already discussed example of creating a user:

POST /user/create

{
  "name": "Остап Бендер",
  "age": 42,
  "email": "bender@mail.ru"
}

The verb POST is chosen correctly. It is with this verb that entities are created. But in the url, “user” is singular, and at the end the verb “create” is used, which is unacceptable, because REST degenerates into RPC. Moreover, the verb “create” is redundant, because the intention to create an entity is indicated by the verb POST. A more correct solution would be:

POST /users

{
  "name": "Остап Бендер",
  "age": 42,
  "email": "bender@mail.ru"
}

Well, okay, it’s clear with verbs in the url – we avoid redundancy. But why is it recommended to use plural nouns? To answer this question, consider an example of obtaining an entity:

GET /users/1

In this example, GET indicates the desire to simply read the entity, “users” specifies which entity we are talking about, and the “1” at the end of the url indicates that the user with ID 1 is interested in. On the other hand, this handle could be designed as follows way:

GET /user/1

There is no redundancy here. It seems that everything is the same as in the first version. The problem is not noticeable as long as we are talking about reading a specific entity. But what if we need to get a list of entities? I think you will agree that the pen

GET /user

somewhat inexpressive. There is a feeling that it will return the first user it comes across. In practice, I have repeatedly seen how this problem was played out:

GET /user/list

But this is a dubious decision, because… “list” is perceived as a separate entity with its own state, which depends on or is somehow related to the “user” entity. So, in order not to interfere with such “crutches”, it is recommended to use plural entities in the url. Then we get:

GET /users/1 – getting user with ID 1

GET /users – getting a list of users

Such a seemingly trifle makes your API more intuitive, and extending this rule to all types of requests standardizes and therefore simplifies the perception of the API.

Harsh reality

And everything seemed to be fine, but when I was discussing these things with colleagues, I was asked a good question: how to beat a situation when you need a pen just to send a notification by email? I would like to note right away that the system contained a user entity and other entities that had nothing to do with notification methods. For such a situation something like this suggests itself:

POST /users/1/emails/send

{
  "to": "balaganov@ya.ru",
  "subject": "Некоторая тема",
  "body": "Что-то весьма интрересное"
}

The verb POST is used as the most appropriate – we have to choose from what we have. From the url it is clear that we are talking about an email notification to a user with ID 1. At the end of the url we see the verb “send”, to understand what action is required. But this is a quick, superficial solution. In fact, here you need not only to “screw on” another API handle, but also to understand that at the moment the system lacks essence. But which one? Let's try to understand. Suppose the email entity is missing. But is email an entity? What is an entity? – Eric Evans in his book “Domain-Driven Design. Structuring Complex Software Systems.” gives a very specific definition: a logically integral object, determined by a set of individual traits, is called an essence. Let's say email is a value object, and in the system under consideration there are 2 users: Ostap Bender and Shura Balaganov. In this case, email does not have any unique properties, that is, it can be assigned to any parent entity. This means that Ostap Bender’s email can be assigned to Shura Balaganov. But then what happens? Shura Balaganov will receive letters addressed to Ostap Bender. And it’s not good to read other people’s letters, so email is an entity, because… The mailbox address is unique. But by adding the email entity to the system, it can hardly be said that the problem is solved:

POST /users/1/emails

{
  "to": "balaganov@ya.ru",
  "subject": "Некоторая тема",
  "body": "Что-то весьма интрересное"
}

The fact is that the REST API offers a set of tools for crud operations on entities. That is, the request POST /users/1/emails is perceived as simply creating a new mailbox for a specific user, but not sending a letter. And generally speaking, this handle should look like:

POST /users/1/emails

{
  "name": "bender@ya.ru"
}

Unfortunately, this is not what we need. We just need to send a letter. How to solve this problem?…Wait, what if we add the “task” entity – task. A task can be created with the following request:

POST /users/1/tasks

{
  "type": "email",
  "data": {
    "to": "balaganov@ya.ru",
    "subject": "Некоторая тема",
    "body": "Что-то весьма интрересное"
  }
}

Although we did not reach the goal, this is much better – the recommendations for REST API design are followed, and we did not find ourselves in a dead end – we got a solution that needs to be further developed. That is, now there are handles for two entities: user, task; with which you can work with database tables:

user

user_id

uuid

name

varchar(255)

age

smallint

email

varchar(255)

task

task_id

uuid

type

varchar(255)

user_id

uuid

data

jsonb

Generally speaking, at this stage you should calm down regarding the REST API, because… her mission is completely completed – a task to send a letter has been created. Directly sending a letter should not be the responsibility of the API. You should use a background process for this action:

Service architecture

Service architecture

With this approach to design, the service will consist of 2 independent modules: API and job. The API will allow you to perform crud operations on entities, and job will allow you to run the corresponding business logic.

Stability and scalability

Is it necessary to complicate the logic so much? Perhaps not for a specific example. Somewhere on the front there is a “notify” or “send email” button. The user clicks on it, the letter is sent; or it is not sent and the error “Something went wrong. Please try again later” appears. For example, there was a problem on the mail server side. What will the user need to do in this case? – at least repeat your actions or contact support and start a scandal. And then someone from support or a calmed user will try to send again. In general, there is no clear answer regarding architecture. Here, you probably need to look at the nervousness of users, and how hard such situations hit the pocket and reputation of the business owner. However, I would like to note that with an architecture with an API and a background process, if problems arise with sending a letter, job can repeat this action a little later. Moreover, in case of repeated errors, the problem can be caught by monitoring the service, then you can correct something in the code and restart the job, which will kindly send the letter. What is the beauty of such a scheme? – the user does not need to be forced to repeat his actions. He can simply highlight the status of sending the letter somewhere in the application interface and reassure him: “Vasya, don’t worry – we’ll figure it out. Everything will be fine!” I think you'll agree that when you want to do something in an application, but you can't do it, it's a little frustrating. But if you are also forced to repeat some actions, then you become more, so to speak…upset. Moreover, even if a technical employee support will do some action for you, then this also may not improve the situation much – in your account there may be some information intended only for your eyes, or it may accidentally break something in your profile. In any case, the taste will remain that someone climbed into your locker and rummaged through your personal belongings – this is not pleasant. And the background process completely eliminates the need to repeat actions on the interface, because everything that needs to be done is recorded in a database table. And the interaction scheme is faster and simpler – the API does not need to wait for a response from the mail server, it simply writes data to the database – the logic is minimized, and there is practically nothing to break here, unless, of course, your API or database fails. So what happens? Following tedious recommendations, namely, using the REST API only for crud operations on entities, we were forced to slightly change the service architecture – add the job module; Ultimately, this step affected the stability and modularity of the service, because received 2 small and independent modules with minimal logic; modularity, in turn, affected the scalability of the service – for n API instances, you can raise m job instances. And, best of all, if problems arise with sending a letter, the lion's share of them can be solved simply by restarting the job module.

Ease of use and support

While designing services of varying complexity, I have repeatedly heard the question: what could be dumber than screwing on a handle for the REST API? It would seem that designing a REST API is not such a difficult matter, and the answer is obvious. But let’s not rush, but try to answer it by considering the following example. Let's say we have a user entity with the fields name, contact, is_active. Moreover, the user can have several contacts: mail, telegram, phone number. The is_active field indicates user activity. An active user has access to more system functionality. Let's assume there is a handle for creating, deleting and reading a user. And then the business asks to add a user activation handle. One of the solutions that immediately comes to mind:

PATCH /users/1/activate

{
  "is_ative": true
}

Accordingly, if you need to deactivate, then:

PATCH /users/1/activate

{
  "is_ative": false
}

I saw it in articles and colleagues said how original this solution was. The output is a human-readable API. Okay, we've screwed on the user activation handle. And after some time they will ask for a pen that will change the username. It's good practice to stick to an existing style:

PATCH /users/1/rename

{
  "name": "Александр Балаганов"
}

And after some time you will need to add new mailboxes automatically. And this solution will be as follows:

PATCH /users/1/new_email

{
  "name": "balaganov_forever@ya.ru"
}

Accordingly, there will be handles for adding telegram and phone numbers. And to change the user entity we will have 5 handles… Watching similar evolutions of systems, I noticed that it was enough to screw just one handle:

PATCH /users/1

{
  "name": "balaganov_forever@ya.ru",
  "is_active": true,
  "contact": [
    {
      "type": "email",
      "name": "balaganov@ya.ru"
    },{
      "type": "email",
      "name": "balaganov_contact@ya.ru"
    },{
      "type": "telegram",
      "name": "@balagan"
    },{
      "type": "phone",
      "name": "8(xxx)-xxx-xx-xx"
    }
  ]
}

1 handle is easier to maintain than 5. Moreover, the job will only be done once. Thus, by designing the REST API as an API for performing crud operations on entities, and not inventing “improvements” in the form of explanations in the url: ativate, rename, new_email, etc., we will get a universal and minimal API. Well, now it’s time to return to the previously asked question: “what could be dumber than screwing on a handle for the REST API.” I came to this answer: “The dumbest thing to do is attach a handle for the REST API to attach a similar handle to an existing one.”

Conclusion

Understanding the REST API as an API for working with entities, as well as following a few and simple recommendations, allows you to design a universal and minimal API, which greatly facilitates development and support. And also, in principle, it can lead to a revision of the architecture, which in turn will ensure high modularity and, as a result, will have a positive impact on the stability and scalability of the service as a whole.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *