Skip to main content

Що таке REST та принципи REST — REST API

REST (Representational State Transfer) - це архітектурний стиль для мережевих застосунків, де клієнти взаємодіють із ресурсами на сервері через стандартні HTTP методи та URI.

Теорія

TL;DR

  • REST схожий на публічну бібліотеку: клієнти запитують ресурси за ID через стандартні методи (GET, POST, PUT, DELETE), сервер керує зберіганням незалежно
  • Головне обмеження - безстанність (statelessness): кожен запит несе весь потрібний контекст (токен авторизації, курсор пагінації), сервер не пам'ятає попередніх запитів
  • REST підходить для публічних API, CRUD-операцій і сервісів, доступних з браузера. Не підходить для двостороннього real-time зв'язку (WebSockets) або швидких внутрішніх сервісів із бінарними даними (gRPC)
  • У REST шість обмежень: клієнт-сервер, безстанність, кешованість, уніфікований інтерфейс, шарова система, код на запит (необов'язково)
  • "RESTful" означає дотримання всіх шести обмежень. Більшість API, що називають себе REST, пропускають HATEOAS і фактично є просто HTTP API

Швидкий приклад

Базовий REST API на Express.js для ресурсу /users:

javascript
const express = require('express'); const app = express(); app.use(express.json()); let users = [{ id: 1, name: 'Alice' }]; app.get('/users', (req, res) => res.json(users)); // Читання всіх app.post('/users', (req, res) => { const user = { id: users.length + 1, name: req.body.name }; users.push(user); res.status(201).json(user); // 201 Created }); app.put('/users/:id', (req, res) => { const user = users.find(u => u.id == req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); user.name = req.body.name; res.json(user); }); app.delete('/users/:id', (req, res) => { users = users.filter(u => u.id != req.params.id); res.status(204).send(); // 204 No Content }); app.listen(3000);

Кожен HTTP метод відповідає одній CRUD-операції. Сервер не зберігає стан між запитами.

Шість обмежень REST

Рой Філдінг описав REST у своїй дисертації 2000 року. Разом ці шість обмежень утворюють передбачувані та масштабовані API.

1. Клієнт-сервер. UI і рівень даних розділені. Клієнт не знає, де живуть дані; сервер не знає, як клієнт відображає відповідь.

2. Безстанність. Кожен запит містить все, що потрібно серверу: токен авторизації, курсор пагінації, тип контенту. Жодного стану на сервері. Це найважливіше обмеження на практиці.

3. Кешованість. Відповіді вказують, чи можна їх кешувати, через заголовки Cache-Control або ETag. Правильне кешування помітно знижує навантаження на сервер.

4. Уніфікований інтерфейс. Чотири підобмеження: ресурси ідентифікуються через URI; взаємодія відбувається через представлення (JSON або XML); повідомлення самоописові (HTTP заголовки несуть тип контенту й авторизацію); HATEOAS (відповіді містять посилання на пов'язані дії).

5. Шарова система. Клієнт звертається до одного ендпоінту, але запит може пройти через балансувальники навантаження, CDN або API-шлюз. Кожен шар бачить лише сусідній.

6. Код на запит (необов'язково). Сервер може надсилати виконуваний код клієнту. Більшість REST API це не використовують.

Обмеження 1-5 - практичний мінімум. HATEOAS - та частина, яку більшість команд пропускає.

Безстанність vs стан: у чому різниця

Безстанність дозволяє REST масштабуватись горизонтально. Будь-який сервер у кластері може обробити будь-який запит, бо немає спільної сесії. Компроміс простий: запити стають більшими. JWT токен важить 500+ байт, а вказівник на серверну сесію - 50 байт. Для публічних API масштабу GitHub або Stripe це виправдано. Для внутрішнього інструменту з двома серверами - менш очевидно.

Коли використовувати REST

  • Публічний API з різними типами клієнтів (браузери, мобільні додатки, сторонні інтеграції): REST
  • Стандартні CRUD-операції над чітко визначеними ресурсами: REST
  • Мікросервіси, де команди використовують різні мови: REST (HTTP - універсальний протокол)
  • Двосторонній real-time зв'язок (чат, live-сповіщення): WebSockets
  • Швидкісні внутрішні сервіси з бінарними даними: gRPC
  • Складні запити, де клієнт сам визначає потрібні поля: GraphQL

Порівняння протоколів

АспектRESTSOAPGraphQL
ПротоколHTTP/1.1 або HTTP/2HTTP, SMTP, TCPHTTP
Формат данихJSON, XMLТільки XMLJSON
СтанБезстаннийМоже бути станБезстанний
Гнучкість запитівФіксовані ендпоінтиXML-конвертиКлієнт визначає
Накладні витратиНизькіВисокіНизькі-середні
Обробка помилокHTTP статус-кодиSOAP faultsКастомні помилки у тілі
Підходить дляВеб API, браузери (Stripe)Корпоративна безпека (банки)Складні запити (стрічка Facebook)

Типові помилки

Найчастіша проблема з REST у продакшені - не неправильні HTTP методи. Команди ламають власну безстанність і не помічають цього.

Зберігання стану на сервері. Класика: sessions[userId].cart = items. Працює на одному сервері. Ламається, коли балансувальник навантаження відправляє наступний запит на іншу машину. Рішення: зберігати стан кошика в JWT і передавати в кожному запиті.

javascript
// Неправильно: ламається з кількома серверами app.post('/cart/add', (req, res) => { sessions[req.sessionId].items.push(req.body); }); // Правильно: безстанний підхід із JWT app.post('/cart/add', (req, res) => { const cart = jwt.verify(req.headers.authorization, secret).cart; cart.items.push(req.body); res.json({ token: jwt.sign({ cart }, secret) }); });

POST для читання даних. POST не є ідемпотентним. Якщо клієнт повторить невдалий POST, може з'явитись дублікат запису. GET для читання: він є безпечним і ідемпотентним за специфікацією HTTP.

200 для всього. Якщо повертаєш { error: 'Not found' } зі статусом 200, інструменти моніторингу, обробники помилок клієнта та API-клієнти пропускають цю помилку. Повертай 404, 422, 500 там, де це доречно.

Відсутність версіонування з самого початку. Міграція Twitter API з v1 на v2 зламала тисячі інтеграцій. Додавай /api/v1/ від першого дня.

HTTP методи як декорація. PUT має бути ідемпотентним: два однакових виклики повинні давати той самий результат. PATCH оновлює частковий стан. DELETE має бути безпечним для повторного виклику. Порушення цих контрактів ламає логіку повторних спроб на стороні клієнта.

Де зустрічається на практиці

  • Stripe: GET /v1/customers/:id повертає дані клієнта з HATEOAS посиланнями на підписки та платежі
  • GitHub API: PUT /repos/:owner/:repo для оновлень, GET /repos?page=2 для пагінації
  • Twitter API v2: POST /2/tweets для створення твітів із масивами медіа
  • Express.js: app.use('/api/v1', router) як стандартна структура версіонованих маршрутів
  • React + Axios: fetch('/api/users', { headers: { Authorization: 'Bearer token' } }) для безстанної авторизації

Питання на співбесіді

Q: Назви шість обмежень REST.
A: Клієнт-сервер, безстанність, кешованість, уніфікований інтерфейс, шарова система, код на запит (необов'язково). Перші п'ять обов'язкові для справжнього RESTful API.

Q: Що таке HATEOAS і чому більшість API його не реалізують?
A: Hypermedia As The Engine Of Application State означає, що відповіді містять посилання на доступні наступні дії. Відповідь на GET /users/1 включала б посилання на редагування і видалення. Більшість команд пропускає це через збільшення розміру відповідей і складності, а клієнти однаково хардкодять URL.

Q: Яка різниця між REST і RESTful?
A: REST - це архітектурний стиль, який описав Філдінг. RESTful означає, що реалізація дотримується всіх шести обмежень. Більшість API, що називають себе REST, насправді є HTTP API без HATEOAS.

Q: Як реалізувати авторизацію без збереження стану?
A: Через JWT Bearer токени. Токен містить ідентифікатор користувача і підписаний на сервері. Кожен запит включає його в заголовку Authorization. Таблиця сесій не потрібна. Для SPA OAuth2 PKCE flow безпечно обробляє обмін токенами.

Q: Спроектуй REST API для фотосервісу з завантаженням файлів понад 2 ГБ, курсорною пагінацією та real-time лайками.
A: POST /photos із multipart-завантаженням і підтримкою відновлення через протокол Tus.io. GET /photos?cursor=abc&limit=20 для keyset-пагінації (стабільна при додаванні нових фото). WebSockets або SSE для real-time лайків, бо REST-поллінг тут надто повільний. ETags плюс CDN для кешування метаданих фото.

Приклади

Базовий: CRUD із правильними HTTP статус-кодами

javascript
const express = require('express'); const app = express(); app.use(express.json()); let users = [{ id: 1, name: 'Alice' }]; app.get('/users', (req, res) => { res.json(users); // 200 OK за замовчуванням }); app.post('/users', (req, res) => { const user = { id: users.length + 1, name: req.body.name }; users.push(user); res.status(201).json(user); // 201 Created, не 200 }); app.put('/users/:id', (req, res) => { const user = users.find(u => u.id == req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); // 404, не 200 user.name = req.body.name; res.json(user); }); app.delete('/users/:id', (req, res) => { users = users.filter(u => u.id != req.params.id); res.status(204).send(); // 204 No Content }); app.listen(3000, () => console.log('Сервер запущено на порті 3000'));

Статус-коди - це не прикраса. 201 повідомляє клієнту, що ресурс створено. 204 сигналізує про успіх без тіла відповіді. 404 перехоплюється обробником помилок. Якщо повертати 200 для всього, стандартні інструменти перестають працювати коректно.

Середній: Безстанна пагінація з Bearer токеном

Клієнт передає авторизацію і пагінацію в кожному запиті. На сервері між запитами нічого не зберігається.

Сервер:

javascript
app.get('/api/v1/users', (req, res) => { const auth = req.headers.authorization?.split(' ')[1]; if (!auth) return res.status(401).json({ error: 'Unauthorized' }); const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const start = (page - 1) * limit; const paginatedUsers = users.slice(start, start + limit); res.json({ data: paginatedUsers, pagination: { page, limit, total: users.length } // Вивід: { data: [{ id: 1, name: 'Alice' }], pagination: { page: 1, limit: 10, total: 1 } } }); });

Клієнт (React):

javascript
useEffect(() => { fetch('/api/v1/users?page=1&limit=10', { headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9...' } }) .then(res => res.json()) .then(data => setUsers(data.data)); }, []);

Токен несе ідентифікатор користувача. Query-рядок несе стан пагінації. До цього запиту сервер не знав про існування цього клієнта.

Просунутий: HATEOAS із ETag-кешуванням

Це ближче до того, що Філдінг насправді описував. Відповіді містять посилання на наступні дії; ETags дозволяють клієнту пропустити завантаження, якщо дані не змінились.

javascript
app.get('/api/v1/users/:id', (req, res) => { const user = users.find(u => u.id == req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); const etag = `"${user.id}-v${user.version || 1}"`; res.set('ETag', etag); res.set('Cache-Control', 'private, max-age=60'); // 304 Not Modified, якщо ETag клієнта збігається if (req.headers['if-none-match'] === etag) { return res.status(304).send(); } res.json({ id: user.id, name: user.name, links: [ { rel: 'self', href: `/api/v1/users/${user.id}`, method: 'GET' }, { rel: 'edit', href: `/api/v1/users/${user.id}`, method: 'PUT' }, { rel: 'delete', href: `/api/v1/users/${user.id}`, method: 'DELETE' } ] }); });

Другий запит від клієнта:

javascript
fetch('/api/v1/users/1', { headers: { 'If-None-Match': '"1-v1"' } }); // Повертає 304, якщо нічого не змінилось - без тіла, менше трафіку

HATEOAS означає, що клієнт дізнається про доступні дії з самої відповіді. Не потрібно вгадувати, які ендпоінти існують. Більшість команд застосовує це вибірково для ресурсоємних ендпоінтів, де важливе кешування або економія трафіку.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?