Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке REST та принципи REST — REST API». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**REST** (Representational State Transfer) - архітектурний стиль для API, де клієнти взаємодіють із ресурсами сервера через стандартні HTTP методи та запити без збереження стану. Кожен запит несе весь потрібний контекст; сервер не зберігає сесію між запитами. ``` GET /users - всі користувачі POST /users - створити (повертає 201) GET /users/1 - один користувач PUT /users/1 - оновити (ідемпотентний) DELETE /users/1 - видалити (повертає 204) ``` **Головне обмеження:** безстанність - токени авторизації та пагінація передаються з кожним запитом, а не зберігаються на сервері.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 ### Порівняння протоколів | Аспект | REST | SOAP | GraphQL | |---|---|---|---| | Протокол | HTTP/1.1 або HTTP/2 | HTTP, SMTP, TCP | HTTP | | Формат даних | JSON, XML | Тільки XML | JSON | | Стан | Безстанний | Може бути стан | Безстанний | | Гнучкість запитів | Фіксовані ендпоінти | 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 означає, що клієнт дізнається про доступні дії з самої відповіді. Не потрібно вгадувати, які ендпоінти існують. Більшість команд застосовує це вибірково для ресурсоємних ендпоінтів, де важливе кешування або економія трафіку.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.