Перейти к основному содержимому

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:

I. Используйте path, а не query:

// Неправильно:
/api?type=chats&id=123

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

II. Используйте в названиях множественное число:

// Неправильно:
/api/chat/123

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

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

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

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

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

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

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

IV. Используйте короткие имена:

// Неправильно:
/api/chat_list

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

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

// Неправильно:
/api/pullRequest

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

VI. Вложенность лучше параметров:

// Неправильно:
/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), какие-то не так удобно (загрузка файлов, бизнес-процессы, сайд-эффекты).