Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Принципи дизайну REST API та найкращі практики?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**REST API** - це підхід, де дані представлені як ресурси з іменниковими URL (`/users/123`), а HTTP-методи визначають дії: `GET` читає, `POST` створює, `PUT`/`PATCH` оновлює, `DELETE` видаляє. Кожен запит stateless. ```javascript GET /users // 200 [{id:1,...}] POST /users // 201 {id:2,...} + заголовок Location PATCH /users/123 // 200 {id:123,...} DELETE /users/123 // 204 No Content ``` **Головне правило:** множина іменників у шляхах, HTTP-методи як дієслова, правильні статус-коди, пагінація в списках.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**REST API** - це набір угод для побудови вебсервісів, де дані представлені як адресовані ресурси з передбачуваними URL, а HTTP-методи визначають, що саме з ними робити. ## Теорія ### TL;DR - Уяви бібліотечний каталог: кожна книга (ресурс) має фіксоване місце на полиці (URL), а дії "взяти" чи "повернути" виконуються стандартними методами (GET/POST) без жодних особливих інструкцій. - Головне правило: іменники в шляхах (`/users`), дієслова через HTTP (`GET` отримує список, `POST` створює, `DELETE` видаляє). - `GET /users/123` читає юзера. `DELETE /users/123` видаляє його. URL не змінюється, тільки метод. - REST для публічних API, де важливі кешування і передбачуваність. GraphQL - коли клієнт потребує гнучкого вибору полів. - Stateless означає, що кожен запит несе повний контекст. Жодних сесій на сервері. ### Швидкий приклад ```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 [{id:1,name:'Alice'}] app.post('/users', (req, res) => { // 201 {id:2,name:'Bob'} const user = { id: users.length + 1, ...req.body }; users.push(user); res.status(201).set('Location', `/users/${user.id}`).json(user); }); app.get('/users/:id', (req, res) => { // 200 або 404 const user = users.find(u => u.id === +req.params.id); user ? res.json(user) : res.status(404).json({ error: 'Not found' }); }); app.delete('/users/:id', (req, res) => { // 204 No Content users = users.filter(u => u.id !== +req.params.id); res.status(204).send(); }); app.listen(3000); ``` `POST` повертає `201 Created` із заголовком `Location`, який вказує на новий ресурс. `DELETE` повертає `204` без тіла відповіді. Це не опціональні конвенції - це те, що очікують HTTP-клієнти і CDN. ### Головна різниця від RPC-стилю RPC-стиль API використовує дієслівні шляхи: `/getUserById`, `/createUser`, `/deleteUser`. REST розглядає URL як іменник, а HTTP-метод як дієслово. `/users/123` - це ресурс. `GET`, `PUT` і `DELETE` - дії над ним. Таке розділення дозволяє CDN та reverse proxy приймати рішення про кешування на основі одного лише методу, без аналізу семантики URL. ### Коли використовувати REST - **Публічний API** - REST. Передбачуваний, кешується, працює з будь-яким HTTP-клієнтом. - **Внутрішній мікросервіс** - REST або gRPC. REST, якщо стандартних HTTP-інструментів достатньо. - **Клієнт потребує гнучкого вибору полів** - GraphQL. Уникає передачі 40 полів, коли потрібно лише 3. - **Оновлення в реальному часі** - WebSockets або SSE. REST не підтримує push за своєю природою. - **Простий CRUD** - REST. Мінімальний поріг входу, запрацює одразу. ### Як HTTP обробляє REST-запити Сервер (Node.js, Nginx або що завгодно) парсить вхідний запит, зіставляє шлях URL з обробником маршруту і виконує логіку на основі `req.method`. Жодних сесій на сервері. Кожен запит несе повний контекст: токен авторизації в заголовку `Authorization`, фільтри в query-параметрах, дані в JSON-тілі. GET-відповіді кешуються CDN за допомогою заголовків `ETag` і `Last-Modified`. Коли клієнт надсилає `If-None-Match` зі збереженим ETag, сервер повертає `304 Not Modified` без тіла, якщо нічого не змінилось. Безкоштовна оптимізація продуктивності без жодного додаткового коду. ### Типові помилки **Дієслівні шляхи.** `/getUsers` або `/deleteUser/123` ламає кешування і плутає HTTP-інструменти. ```javascript // Неправильно app.get('/getAllUsers', handler); app.get('/deleteUser/:id', handler); // Правильно app.get('/users', handler); app.delete('/users/:id', handler); ``` **Однина замість множини.** `/user` замість `/users` створює неоднозначність. `POST /user` - це створення нового юзера чи звернення до поточного? Завжди використовуй множину. **Неправильні статус-коди при створенні.** Повернути `200` замість `201` - помилка. Клієнти й автоматизовані інструменти очікують `201 Created` із заголовком `Location`. ```javascript // Неправильно res.status(200).json(newUser); // Правильно res.status(201).set('Location', `/users/${newUser.id}`).json(newUser); ``` **Відсутність pagination у списках.** `GET /users` без обмеження повертає всі записи і впаде на будь-якій реальній базі даних. За замовчуванням використовуй `?page=1&limit=20`. **Сесії на сервері.** Зберігання стану сесії порушує stateless-принцип і ускладнює горизонтальне масштабування. Використовуй JWT в заголовку `Authorization: Bearer <token>`. **Плутанина між PUT і PATCH.** `PUT` повністю замінює ресурс. Якщо надіслати тільки `{ name: 'Bob' }` в `PUT /users/1` і обробник виконає повну заміну, email юзера зникне. `PATCH` - для часткових оновлень (часткове оновлення, partial update). Більшість команд використовують `PATCH` для редагування і залишають `PUT` для випадків, де повна заміна є навмисною. ### Де зустрічається в реальних проектах - **Stripe API** - `/v1/customers`, `GET` зі cursor-пагінацією, `POST` із idempotency key в заголовку. - **GitHub API** - `/repos/{owner}/{repo}/issues`, посилання (HATEOAS) `_links` для навігації між сторінками. - **Twitter API v2** - `/2/tweets?expansions=author_id`, query-параметри для вибору полів. - **Express + Prisma** - множина іменників, HTTP-методи, `prisma.user.findMany()` прямо відповідає `GET /users`. - **Версіонування в продакшені** - URL-підхід (`/api/v1/users`) простіший для налаштування proxy. Header-підхід (`Accept: application/vnd.myapi.v2+json`) чистіший, але складніший для тестування в браузері. ### Питання на співбесіді **Q:** Чому множина іменників, а не однина? **A:** Множина природно відображає колекції. `POST /users` створює юзера і повертає `/users/123`. З `/user` незрозуміло: ти створюєш нового юзера чи звертаєшся до поточного? **Q:** Які HTTP статус-коди відповідають операціям CRUD? **A:** `GET` повертає `200` (знайдено) або `404` (не знайдено). `POST` повертає `201 Created` із `Location`. `PUT` і `PATCH` повертають `200` (з тілом) або `204` (без тіла). `DELETE` повертає `204 No Content`. **Q:** У чому різниця між PUT і PATCH? **A:** `PUT` повністю замінює ресурс, `PATCH` застосовує часткові зміни. GitHub використовує `PATCH /repos/:id`, щоб оновити тільки опис репозиторію без перезапису інших полів. **Q:** Як організувати версіонування API? **A:** Два основних підходи: префікс в URL (`/api/v1/users`) і заголовок (`Accept: application/vnd.myapi.v2+json`). URL-версіонування простіше для маршрутизації та дебагу. Header-версіонування чистіше, але потребує налаштування на клієнті. **Q:** Як працює кешування в REST? **A:** Сервер встановлює `ETag` (хеш відповіді) на GET-відповідях. Клієнт зберігає його і надсилає `If-None-Match: <etag>` в наступному запиті. Якщо ресурс не змінився, сервер повертає `304 Not Modified` без тіла. CDN роблять це автоматично для публічних ендпоінтів. **Q:** (Senior) Спроектуй REST API для блогу з постами, коментарями, пошуком та авторизацією. Які ендпоінти, заголовки та статус-коди ти б використав? **A:** Починай із `GET /posts?page=1&search=foo`, який повертає `{ data: [], _links: { next: { href: '...' } } }`. Авторизація через `Authorization: Bearer <jwt>`. Створення через `POST /posts` з поверненням `201` та `Location`. Коментарі на `GET /posts/:id/comments`. Ліміти запитів через заголовки `X-RateLimit-Limit` і `X-RateLimit-Remaining`. HATEOAS-посилання в кожній відповіді, щоб клієнти могли навігувати без жорсткого кодування URL. ## Приклади ### Базовий CRUD для ресурсу користувача ```javascript const express = require('express'); const app = express(); app.use(express.json()); let users = [ { id: 1, name: 'Alice', email: 'alice@example.com' } ]; // GET /users - список всіх app.get('/users', (req, res) => res.json(users)); // GET /users/:id - отримати одного app.get('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); user ? res.json(user) : res.status(404).json({ error: 'User not found' }); }); // POST /users - створити app.post('/users', (req, res) => { const user = { id: users.length + 1, ...req.body }; users.push(user); res.status(201).set('Location', `/users/${user.id}`).json(user); }); // PATCH /users/:id - часткове оновлення app.patch('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); Object.assign(user, req.body); // злиття змін, не заміна res.json(user); }); // DELETE /users/:id app.delete('/users/:id', (req, res) => { users = users.filter(u => u.id !== +req.params.id); res.status(204).send(); }); app.listen(3000); ``` Кожен метод відповідає одній дії. URL (`/users/:id`) залишається незмінним, змінюється тільки HTTP-метод. `PATCH` використовує `Object.assign` для злиття змін, а не перезапису. `DELETE` повертає `204` без тіла, бо описувати вже нічого. ### Пагінація і фільтрація ```javascript app.get('/users', (req, res) => { const { page = 1, limit = 10, name } = req.query; let filtered = users; if (name) filtered = filtered.filter(u => u.name.includes(name)); const start = (page - 1) * limit; const data = filtered.slice(start, +start + +limit); res.json({ data, pagination: { page: +page, limit: +limit, total: filtered.length, pages: Math.ceil(filtered.length / limit) } }); }); // curl "localhost:3000/users?page=1&limit=2&name=Alice" // -> { data: [{id:1,name:'Alice',...}], pagination: {page:1,limit:2,total:1,pages:1} } ``` Ніколи не повертай необмежений список. Будь-який ендпоінт, що звертається до таблиці бази даних без `LIMIT`, впаде під навантаженням. Цей патерн відповідає підходу Stripe і GitHub: масив `data`, метадані пагінації, опціональна фільтрація через query-параметри. Я бачив цей баг у продакшені: `GET /orders`, який чудово працював в розробці і падав по таймауту в перший понеділок після деплою. ### HATEOAS-посилання і часткове оновлення через JSON Patch ```javascript // PATCH з JSON Patch (RFC 6902) - масив операцій app.patch('/users/:id', (req, res) => { const user = users.find(u => u.id === +req.params.id); if (!user) return res.status(404).json({ error: 'User not found' }); // req.body: [{ op: 'replace', path: '/name', value: 'Alicia' }] req.body.forEach(op => { if (op.op === 'replace') user[op.path.slice(1)] = op.value; }); res.json({ ...user, _links: { self: { href: `/users/${user.id}` }, posts: { href: `/users/${user.id}/posts` } } }); }); // curl -X PATCH localhost:3000/users/1 \ // -H "Content-Type: application/json" \ // -d '[{"op":"replace","path":"/name","value":"Alicia"}]' // -> { id: 1, name: 'Alicia', _links: { self: {href:'/users/1'}, posts: {href:'/users/1/posts'} } } ``` JSON Patch (RFC 6902) надсилає масив операцій замість часткового об'єкта. Намір явний: ти не кажеш "ось як зараз виглядає об'єкт", а кажеш "застосуй цю конкретну зміну". Поле `_links` за підходом GitHub: клієнти знаходять пов'язані ресурси без жорсткого кодування URL.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.