Skip to main content

REST

Зачем?

Как говорилось в предыдущем уроке, HTTP — это текстовый протокол, по которому можно передавать любые данные с клиента на сервер и обратно. Но такая свобода может быть пагубной — без правил взаимодействия бэкэнда и фронтенда начнётся бардак. Чтобы упорядочить свободу, были придуманы различные подходы и протоколы поверх HTTP. Один из них REST.

Архитектурный стиль, позволяющий сделать сетевое взаимодействие удобнее, прозрачнее и стандартизованнее. (Рой Филдинг)

REST (Representational State Transfer — «передача состояния представления») определяет такие понятия, как ресурсы, представление для ресурсов, идентификатор ресурсов, а также описывает требования к взаимодействию (кеширование, связанность).

HTTP идеально подходит для применения данной архитектуры.


Термины

Ресурсы

В архитектуре REST все действия, которые совершает фронтенд, направлены на работу с ресурсами или сущностями. В курсе будем использовать слово «ресурс». Ресурс — это любой объект бизнес-логики, с которым работает приложение. Пользователь, сообщение, аватарка, товар, категория товара, любой объект реального мира — это ресурсы в REST.

Идентификаторы ресурсов

Ресурсы в REST существуют не просто сами по себе и без цели, нет. Они нужны, чтобы фронтенд что-то с ними делал. Для этого фронтенду нужно знать, как отличать одни ресурсы от других, и для этой цели служат идентификаторы — уникальные значения, которые есть у каждого ресурса, зачастую обозначающиеся полем id. Идентификаторы также используются для построения адреса запроса, об этом через пару абзацев.

Действия с ресурсами

Работа любого приложения так или иначе сводится к манипуляции данными, и REST эти манипуляции стандартизирует, определяя, что можно делать с ресурсами:

  • создавать,
  • получать,
  • обновлять,
  • удалять.

Набор действий маленький и работать с ним просто. Но также это приносит определённые сложности — вам придётся мыслить категориями «ресурс» и «действие». В эти категории плохо укладываются бизнес-действия, такие как, например, «выплатить сотрудникам зарплату» — придётся придумывать сущности для этого действия и создавать их. Иногда это бывает излишней сложностью, из-за которой от REST отказываются, если приложение богато сложной бизнес-логикой.


Запросы

Действия с ресурсами перекладываются на HTTP с помощью методов запросов — каждому действию соответствует свой метод. О том, какой метод HTTP, что означает в REST, будет ниже, а сейчас поговорим о том, на какие адреса эти запросы должны делаться.

Адреса запросов в REST формируются по принципу /ресурс(/id(/ресурс...)?)?. Такие адреса иногда называется эндпоинтами (endpoint).

Выглядит сложно, разберёмся на примерах. Ресурсы могут выстраиваться в иерархии, и эти иерархии вы отображаете в адресе запроса.

Нужен список чатов? Ресурс — чат. Принцип /ресурс. Запрос на /chats.

Нужна информация о конкретном чате? Ресурс — чат. Конкретный чат — идентификатор. Принцип /ресурс/идентификатор. Запрос /chats/123.

Нужен список всех сообщений в чате? Ресурс — сообщение. Где находится этот ресурс? В другом ресурсе — в чате. В конкретном чате — за это отвечает идентификатор. Принцип /ресурс/идентификатор/ресурс. Запрос /chats/123/messages.

Нужно получить информацию о том, что можно сделать с конкретным сообщением в конкретном чате? Ресурс — действия. Где находится этот ресурс? В другом ресурсе — в сообщении. В каком сообщении — идентификатор сообщения. Где находится ресурс сообщения? В другом ресурсе — в чате. В каком чате — идентификатор чата. Принцип /ресурс/идентификатор/ресурс/идентификатор/ресурс. Запрос /chats/123/messages/456/actions.

И так далее.

Но в таких длинных цепочках есть своя опасность для бэкэнда, и, возможно, иногда не стоит держать ресурс «сообщения» в ресурсе «чат» для работы с сообщениями. Для этого может быть достаточно отдельного эндпоинта /messages, чтобы строить запросы от него /messages/456/actions.

Идемпотентность

Так называется свойство, при котором повторный запрос даёт тот же результат, что и первый. Оно не является свойством по умолчанию и не гарантируется сервером, то есть появляется как результат реализации запроса.

Идемпотентные методы:

  • GET,
  • OPTIONS,
  • HEAD,
  • PUT,
  • DELETE.

Методы POST и PATCH — не идемпотентны.

Может показаться «Как же так?! А если между двумя GET-запросами будет PATCH — результаты GET будут разные». Идемпотентность не про это. Она про то, что идемпотентные запросы не должны менять состояние ресурсов на сервере. Бэкэнд-разработчики должны быть осведомлены об этом и не нарушать это свойство. GET - У него нет побочных эффектов, то есть если сделать идентичный GET-запрос несколько раз подряд, то эффект будет одинаковым.


Семантика

GET

Получает состояние ресурса в одном из представлений (JSON, XML, HTML), не содержит тела:

GET /chats  // получить все чаты
GET /chats/123 // получить информацию о чате с идентификатором 123
GET /chats/123/messages // получить все сообщения из чата с идентифиакатором 123
GET /chats?limit=10 // получить 10 чатов

200 Ok // Получили успешно

404 Not found // Ресурс не найден
400 Bad request /chats?limit=muahahaha // Запрос составлен клиентом некорректно

POST

Создаёт новый ресурс с начальным состоянием, когда сервис не знает его ID, содержит тело:

POST /chats  // создать новый чат
{
"data": {...}
}

201 Created // Успешно создано

409 Conflict // Такой документ уже есть (например, пользователь с таким email уже зарегистрирован)

PUT

Создаёт новый ресурс с начальным состоянием, когда сервис знает его ID. Целиком обновляет состояние текущего ресурса, содержит тело:

PUT /chats/123  // создать новый чат с идентификатором 123
{
"data": {...}
}

200 Ok // Выполнено успешно
204 No content

404 Not found // Ресурс не найден

DELETE

Удаляет существующий ресурс, не содержит тела:

DELETE /chats/123  // удалить чат с идентификатором 123
DELETE /chats/123/messages // удалить все сообщения из чата с идентификатором 123

200 Ok
204 No content

404 Not found

PATCH

Частично обновляет состояние существующего ресурса, содержит тело:

PATCH /chats/123
{
"data": {...}
}

200 Ok
204 No content

404 Not found

Про отличия PATCH и PUT можно почитать в стандарте.


Запрашивает заголовки, чтобы проверить существование ресурса, не содержит тела:

HEAD /chats/123

200 Ok

404 Not found

OPTIONS

Запрашивает правила взаимодействия, например, доступные методы, не содержит тела:

OPTIONS /search

204 No content
Allow: OPTIONS, GET, HEAD

POST /search

405 Method not allowed

Лучшие практики в REST

Как можно делать правильно, но не описано в REST:

  1. Используйте path, а не query:
// Неправильно:
/api?type=chats&id=123

// Правильно:
/api/chats/123

  1. Используйте в названиях множественное число:
// Неправильно:
/api/chat/123

// Правильно:
/api/chats/123

  1. Используйте только существительные.

В HTTP методы отвечают за действия:

  • POST — создать,
  • GET — получить.

Глагол здесь — избыточная информация.

// Неправильно:
POST /api/chats/add

// Правильно:
POST /api/chats

  1. Используйте короткие имена:
// Неправильно:
/api/chat_list

// Правильно:
/api/chats

  1. Используйте строчные буквы. Это общепринятая форма отображения URL в браузере, даже если в проекте на JavaScript принят camelCase:
// Неправильно:
/api/pullRequest

// Правильно:
/api/pull-request

  1. Вложенность лучше параметров:
// Неправильно:
/api/messages?chat_id=213

// Правильно:
/api/chats/123/messages



Примеры REST

Ответственный фронтенд-разработчик наравне с бэкендерами принимает участие в проектировании протокола взаимодействия клиента и сервера (проще говоря, они договариваются о «ручках»). В этом уроке рассмотрим процесс разработки API с использованием REST для двух задач.

Задача про аватарку пользователя

Практически каждое веб-приложение сегодня, которое позволяет регистрироваться, позволяет и устанавливать аватарку. Наша задача — спроектировать REST API для работы с аватаркой.

Предположим, у нас уже есть простейшая модель пользователя:

{
"id": "string",
"login": "string",
"password": "string"
}

Перегружать модель дополнительными полями не будем — это не важно сейчас.

Какие действия нужны изначально? Регистрация, вход, изменение логина, изменение пароля. Прежде чем перейти к аватарке, посмотрим, как эти действия укладываются в REST.


Регистрация

С регистрацией всё просто, ведь по сути регистрация — это создание пользователя.

POST /users HTTP/1.1
{
"login": "...",
"password": "..."
}

Достойная тема для обсуждения: что должен возвращать ответ на создание пользователя? Модель пользователя с ID? Просто статус ответа без тела? Зависит от обстоятельств, и вы должны решить это сами. В общем случае кажется, что от регистрации достаточно кода ответа без тела, потому что после пользователь всё равно должен совершить вход. Или должен быть сразу авторизован?


Вход

Независимо от того, что вы решите по регистрации, — авторизовать ли сразу после регистрации или нет, вход представляет собой интересную задачу в терминах REST.

Вы помните, что в REST есть ресурсы и действия с ними. Ключевое слово — ресурсы. И тут впервые приходится задуматься, как описать «вход» в нужных терминах, — какой это ресурс и какое действие?

Обычно всё сводится к тому, что вход — это работа с активной сессией. Вход — создание сессии. Выход — удаление сессии. Пролонгирующие запросы — обновление сессии.

POST /session HTTP/1.1
{
"login": "...",
"password": "..."
}

// ответ:
HTTP/1.1 201 CREATED
{
"token": "...."
}

Вот таким образом «вход пользователя в систему» превратился в «создание сессии».


Изменение данных пользователя

Здесь наконец-то подбираемся к аватарке. Во-первых, договариваемся с бэкэндом, что у пользователя появляется новое поле avatar, по умолчанию null, при регистрации не указывается, содержит ссылку на изображение. Теперь задача изменения данных пользователя включает в себя и аватарку без каких-то особенностей: есть ресурс «пользователь», есть действие «обновить данные». Какие данные отправим (логин, пароль, аватар или их сочетания) — такие и будут.

PATCH /users HTTP/1.1
{
"login": "....",
"password": "...",
"avatar": ???
}

Маловероятно, что интерфейс будет предлагать пользователю вставить ссылку на аватарку. Скорее всего, пользователю будет предложено выбрать файл со своего компьютера.

И тут появляется интересный момент: отправлять на сервер аватарку мы будем с помощью content-type: multipart/form-data или кодировать изображение в base64, а получать как ссылку. Уже получается нечестно, данные в одну сторону (фронт → бэк) идут в одном виде, а назад (бэк → фронт) приходят в другом. Способы разобраться с этим есть, наша же задача сейчас показать вам, что такое бывает, а не как с этим справиться (потому что REST богат на подобные задачи).


Доработка — пропорции аватарки

Менеджеру приходит отличная мысль: пусть пользователь может при выборе файла с аватаркой также настроить размер и положение зоны внутри фотографии, которая будет миниатюрой аватарки. Для решения этой задачи на бэкэнд нужно передавать дополнительные поля. Если расширить модель пользователя, она будет выглядеть раздуто и перестанет отвечать своей основной цели: отвечать только за пользователя.

{
"id": "string",
"login": "string",
"password": "string",
"avatar": "string",
"x": number,
"y": number,
"width": number,
"height": number
}

Логично будет вынести аватар в отдельный ресурс и убрать все связанные с ним поля из модели пользователя:

{
"avatar": "string",
"x": number,
"y": number,
"width": number,
"height": number
}

Так как теперь есть отдельный ресурс и этот ресурс принадлежит пользователю, — можно сконструировать эндпоинт для аватара: /users/:id/avatar. Тут появляется новый вопрос: в соответствии с REST, с ресурсом аватара надо будет работать как с коллекцией, так как GET /users/:id/avatar возвращает список ресурсов, а POST будет создавать аватар и присвоит ему ID. Конечно, можно попросить бэкенд сделать исключение для аватаров и работать с этим эндпоинтом нестандартно, но это головная боль для бэкендеров. Чтобы всё было честно, можно сделать установку аватара следующим образом:

  • создать аватар запросом POST на эндпоинт /users/:id/avatar,
  • полученный идентификатор аватара установить в поле avatar пользователю запросом PATCH.

Либо договориться с бэкэндом, чтобы GET /users/:id/avatar в списке первым возвращал актуальный аватар (на случай, если пользователь много раз менял аватар, а предыдущие не удалялись).


Заключение

Будьте готовы к таким ухищрениям с представлением действий в виде ресурсов при работе с REST. Надо понимать, что REST — не «серебряная пуля» и не абсолютный стандарт: какие-то вещи с ним делать удобно (всё, что касается CRUD), какие-то не так удобно (загрузка файлов, бизнес-процессы, сайд-эффекты).