REST на практике: ресурсы, версии, пагинация
REST это не «JSON по HTTP». Это набор ограничений, из которых следуют вполне конкретные решения: как назвать URL, какой метод выбрать, какой статус вернуть и что делать, когда клиент нажал «купить» дважды. В этом уроке проектируем API целиком: от ограничений Филдинга до сквозного примера с билетами, который дальше станет твоим ДЗ.
Семантику HTTP (методы, статусы, заголовки) ты уже знаешь из урока про основы HTTP. Здесь мы поднимаемся на уровень выше: не «что значит 409», а «когда его проектировать».
REST constraints: откуда всё взялось
В 2000 году Рой Филдинг, один из авторов HTTP, защитил диссертацию, в которой описал архитектурный стиль веба и назвал его REST. Не библиотеку и не стандарт, а шесть ограничений. Если система их соблюдает, она получает свойства веба: масштабируемость, устойчивость к эволюции, кешируемость.
- Client-server. Разделение ролей, ты разобрал его в прошлом уроке.
- Stateless. Сервер не помнит контекст между запросами, каждый запрос самодостаточен. Оттуда же.
- Cache. Ответы помечены как кешируемые или нет. Целый урок про кеширование уже был.
- Uniform interface. Единый способ работы с любым ресурсом: одни и те же методы, одни и те же статусы. Это сердце REST, и почти весь сегодняшний урок про него.
- Layered system. Между клиентом и сервером могут стоять прокси, балансировщики и CDN, и никто не обязан об этом знать.
- Code on demand (опциональное). Сервер может отдать клиенту код. В API-мире почти не используется.
Из uniform interface вытекает и самая известная неиспользуемая идея: HATEOAS. Ответ должен содержать ссылки на возможные действия: получил билет, внутри ссылка на рефанд. Браузер с человеком так и работает (страница содержит ссылки), а вот API-клиенты в реальности пишутся под захардкоженные URL из документации. HATEOAS остался в диссертации и в редких API; знать о нём нужно, строить на нём в 2026 обычно нет.
Чтобы оценить API, есть готовая карта: шкала зрелости Ричардсона:
- Уровень 0: один URL, всё через POST (так выглядит RPC поверх HTTP).
- Уровень 1: появились ресурсы с отдельными URL.
- Уровень 2: методы и статусы HTTP используются по назначению.
- Уровень 3: hypermedia.
Здоровая цель для продуктового API это уверенный уровень 2. Шкала это карта местности, а не лестница добродетели: подниматься на третий уровень ради галочки не нужно.
REST против RPC
Альтернативный стиль,
RPC,
смотрит на API как на набор процедур: createTicket, refundTicket,
getMyTickets. REST смотрит как на набор ресурсов, над которыми
выполняются стандартные операции: POST /tickets, GET /me/tickets.
Разница не косметическая. В REST-стиле клиент и инфраструктура заранее
знают семантику операций: GET можно кешировать и повторять, DELETE
идемпотентен, POST опасен для повтора. В RPC-стиле каждая процедура
уникальна, и о её свойствах нужно читать документацию. Зато RPC честнее
для операций, которые плохо ложатся на «существительное плюс CRUD»:
recalculateRating, mergeAccounts. Когда таких операций большинство,
не мучай REST, возьми RPC-стиль явно (gRPC из
следующего урока ровно об
этом).
Ресурсный роутинг
Главное правило uniform interface на практике: URL называет ресурс, метод называет действие. URL это существительное, глагол уже есть в методе.
плохо (глаголы в URL): хорошо (ресурсы):
POST /createTicket POST /tickets
GET /getTicket?id=42 GET /tickets/42
POST /deleteTicket?id=42 DELETE /tickets/42
POST /updateTicketPrice PATCH /tickets/42
Вложенность выражает принадлежность: /events/7/tickets это билеты
события 7. Правило глубины: максимум два уровня. URL вида
/users/1/events/7/tickets/42/payments неудобен и хрупок; если у
ресурса есть глобальный идентификатор, давай ему плоский адрес
/tickets/42, а связи выражай в теле ответа.
Краткое напоминание семантики методов (подробности в уроке про HTTP):
| Метод | Семантика | Safe | Идемпотентный |
|---|---|---|---|
| GET | прочитать ресурс | да | да |
| POST | создать / выполнить | нет | нет |
| PUT | заменить целиком | нет | да |
| PATCH | изменить частично | нет | нет (по умолчанию) |
| DELETE | удалить | нет | да |
Safe и идемпотентность это не теория: на них опирается вся инфраструктура. Прокси смело повторяют GET при сетевой ошибке, потому что он safe. Балансировщик не повторит POST, потому что не знает, можно ли. Когда ты нарушаешь семантику (меняешь данные на GET), ты ломаешь договор не с клиентом, а со всем интернетом между вами.
Действия, которые не ложатся в CRUD
Рано или поздно появляется операция-глагол: рефанд, отмена, публикация.
Втискивать её в CRUD получается криво: PATCH /tickets/42 с телом
{ status: 'refunded' } прячет важную бизнес-операцию за обычным
редактированием, как будто рефанд это то же, что исправить опечатку.
Принятый компромисс: оформить действие под-ресурсом:
POST /tickets/42/refund
POST /articles/7/publish
POST /accounts/3/merge
Это честнее, чем ?action=refund, по трём причинам: у операции свой
URL (можно отдельно логировать, отдельно ограничивать права, отдельно
документировать), метод POST честно говорит «не повторяй вслепую», и
читается как фраза: «создать рефанд для билета 42».
Статусы: проектируем, а не выбираем наугад
Уровень 2 по Ричардсону это когда статусы несут смысл. Рабочий набор для API небольшой:
- 200 OK: успех, в теле результат.
- 201 Created: создали ресурс. Хороший тон: заголовок
Locationс адресом созданного. - 204 No Content: успех без тела. Естественный ответ DELETE.
- 400 Bad Request: запрос синтаксически кривой (невалидный JSON, мусор в параметрах).
- 422 Unprocessable Content: запрос синтаксически корректный, но не проходит бизнес-валидацию (email занят, дата в прошлом). Граница с 400 условна; главное, выбери конвенцию и держись её во всём API.
- 401 Unauthorized: мы не знаем, кто ты. Имя исторически неудачное, по смыслу это unauthenticated.
- 403 Forbidden: мы знаем, кто ты, и тебе нельзя.
- 404 Not Found: ресурса нет. Иногда возвращается вместо 403, чтобы не подтверждать само существование ресурса.
- 409 Conflict: запрос противоречит текущему состоянию (билет уже отрефанжен, место занято, версия устарела).
Тело ошибки тоже часть контракта. Не выдумывай формат на каждый эндпоинт; есть стандарт Problem Details:
{
"type": "https://api.example.com/errors/sold-out",
"title": "Билеты закончились",
"status": 409,
"detail": "На событие 7 продано 500 из 500 мест",
"instance": "/events/7/tickets"
}
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.