Пошук уроків, статей та іншого контенту
Навчитеся проєктувати ресурси, URI, HTTP-методи та відповіді API для узгодженого й передбачуваного інтерфейсу.
REST API — це інтерфейс, через який клієнт взаємодіє з ресурсами сервера за допомогою HTTP.
Ресурс — це сутність предметної області, наприклад:
книга;
користувач;
замовлення;
коментар.
REST API описує:
як називаються ресурси;
за якими URI до них звертатися;
який HTTP-метод використовувати;
який формат мають запити й відповіді;
які статус-коди повертати.
Наприклад, API для книг може мати такі операції:
GET /api/v1/books — отримати список книг;
GET /api/v1/books/42 — отримати книгу з ідентифікатором 42;
POST /api/v1/books — створити книгу;
PATCH /api/v1/books/42 — частково оновити книгу;
DELETE /api/v1/books/42 — видалити книгу.
Клієнту не потрібно знати внутрішню реалізацію сервера. Він працює з узгодженими URI, методами та відповідями.
Першим кроком потрібно визначити ресурси, з якими працюватиме API.
books/books
/books/42Для колекцій ресурсів використовують форму множини:
/users
/orders
/commentsЦе дає змогу відрізняти колекцію від одного елемента:
/books — усі книги;
/books/42 — конкретна книга.
У REST URI зазвичай не містить дієслів:
GET /books
POST /books
DELETE /books/42Не рекомендовано проєктувати URI так:
GET /getBooks
POST /createBook
POST /deleteBook/42Дію вже описує HTTP-метод. Тому POST /books означає створення книги, а DELETE /books/42 — її видалення.
Щоб API було передбачуваним, варто дотримуватися єдиного стилю:
використовувати нижній регістр;
розділяти слова дефісами;
використовувати ідентифікатори в URI;
не додавати випадкові варіанти назв.
Наприклад:
/api/v1/book-reviewsКраще не змішувати стилі:
/api/BookReviews
/api/book_reviews
/api/bookReviewsGET використовується для отримання ресурсу або колекції ресурсів.
GET /api/v1/books
GET /api/v1/books/42Запит GET не повинен змінювати дані на сервері.
Типові відповіді:
200 OK — дані успішно отримано;
404 Not Found — окремий ресурс не знайдено.
POST використовується для створення нового ресурсу в колекції.
POST /api/v1/books
Content-Type: application/json
{
"title": "Кобзар",
"author": "Тарас Шевченко"
}Успішне створення зазвичай повертає:
201 Created;
створений ресурс у тілі відповіді;
за можливості — заголовок Location з URI нового ресурсу.
PUT використовується для повної заміни ресурсу.
PUT /api/v1/books/42
Content-Type: application/json
{
"title": "Кобзар",
"author": "Тарас Шевченко",
"year": 1840
}Якщо API використовує PUT, клієнт має передати повне представлення ресурсу. Відсутнє поле може означати, що його потрібно видалити або встановити значення за замовчуванням.
PATCH використовується для часткового оновлення ресурсу.
PATCH /api/v1/books/42
Content-Type: application/json
{
"year": 1840
}У цьому випадку змінюється лише поле year, а решта полів залишається без змін.
DELETE видаляє ресурс.
DELETE /api/v1/books/42Поширена успішна відповідь:
204 No Content — ресурс видалено, тіло відповіді відсутнє.
Для колекції та окремого ресурсу використовують різні URI:
GET /api/v1/books # список книг
POST /api/v1/books # створення книги
GET /api/v1/books/42 # одна книга
PATCH /api/v1/books/42 # часткове оновлення
DELETE /api/v1/books/42 # видаленняДля отримання окремого ресурсу ідентифікатор повинен бути однозначним. Це може бути число, UUID або інше значення, прийняте в конкретному проєкті.
Якщо один ресурс належить іншому, URI може показувати цей зв’язок:
GET /api/v1/books/42/reviews
POST /api/v1/books/42/reviewsТакий URI означає відгуки, пов’язані з книгою 42.
Вкладені URI варто використовувати лише тоді, коли зв’язок справді важливий для операції. Надмірно глибока вкладеність ускладнює API:
/users/1/orders/2/items/3/reviews/4Публічний API може змінюватися. Щоб старі клієнти не перестали працювати після несумісних змін, використовують версію:
/api/v1/books
/api/v2/booksВерсія не повинна змінюватися для кожної маленької правки. Зазвичай нову версію створюють, коли змінюється формат запитів або відповідей і старий клієнт більше не може працювати коректно.
Для REST API часто використовують JSON.
Запит на створення книги:
{
"title": "Кобзар",
"author": "Тарас Шевченко"
}У відповіді бажано повертати стабільну структуру. Наприклад:
{
"id": 1,
"title": "Кобзар",
"author": "Тарас Шевченко"
}Для списку ресурсів можна повертати масив:
[
{
"id": 1,
"title": "Кобзар",
"author": "Тарас Шевченко"
},
{
"id": 2,
"title": "Лісова пісня",
"author": "Леся Українка"
}
]Під час роботи з JSON сервер повинен встановлювати заголовок:
Content-Type: application/jsonПараметри запиту використовують для фільтрації, пошуку, сортування та пагінації:
GET /api/v1/books?author=Shevchenko
GET /api/v1/books?limit=10&offset=20
GET /api/v1/books?sort=titleURI при цьому залишається URI колекції, а параметри змінюють спосіб її представлення.
Статус-код повідомляє клієнту результат операції.
200 OK — запит успішно виконано, відповідь містить дані;
201 Created — створено новий ресурс;
204 No Content — операцію виконано, але тіло відповіді відсутнє.
400 Bad Request — запит має неправильний формат або не проходить перевірку;
404 Not Found — ресурс не знайдено;
405 Method Not Allowed — цей HTTP-метод не підтримується для URI;
409 Conflict — запит конфліктує з поточним станом даних.
500 Internal Server Error — непередбачена помилка на сервері.
Помилку краще повертати у структурованому форматі:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Поле title є обов'язковим"
}
}Структура помилок має бути однаковою в усьому API. Тоді клієнту не потрібно обробляти кожну помилку окремим способом.
Нижче наведено невеликий сервер без додаткових бібліотек. Він працює з колекцією книг у пам’яті.
Створіть файл server.js:
const http = require('node:http');
const { URL } = require('node:url');
const books = [
{
id: 1,
title: 'Кобзар',
author: 'Тарас Шевченко',
},
{
id: 2,
title: 'Лісова пісня',
author: 'Леся Українка',
},
];
let nextId = 3;
function sendJson(response, statusCode, data, headers = {}) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
...headers,
});
response.end(JSON.stringify(data));
}
function sendError(response, statusCode, code, message) {
sendJson(response, statusCode, {
error: {
code,
message,
},
});
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
let body = '';
request.on('data', (chunk) => {
body += chunk;
// Обмежуємо розмір тіла запиту одним мегабайтом.
if (body.length > 1024 * 1024) {
reject(new Error('REQUEST_TOO_LARGE'));
request.destroy();
}
});
request.on('end', () => {
if (body.length === 0) {
resolve({});
return;
}
try {
resolve(JSON.parse(body));
} catch {
reject(new Error('INVALID_JSON'));
}
});
request.on('error', reject);
});
}
const server = http.createServer(async (request, response) => {
const url = new URL(
request.url,
`http://${request.headers.host || 'localhost'}`
);
const pathParts = url.pathname.split('/').filter(Boolean);
// Підтримуємо лише URI виду /api/v1/books або /api/v1/books/:id.
if (
pathParts[0] !== 'api' ||
pathParts[1] !== 'v1' ||
pathParts[2] !== 'books' ||
pathParts.length > 4
) {
sendError(response, 404, 'NOT_FOUND', 'Маршрут не знайдено');
return;
}
const id = pathParts.length === 4 ? Number(pathParts[3]) : null;
if (id !== null && !Number.isInteger(id)) {
sendError(response, 400, 'INVALID_ID', 'Ідентифікатор має бути цілим числом');
return;
}
try {
if (request.method === 'GET' && id === null) {
sendJson(response, 200, books);
return;
}
if (request.method === 'GET' && id !== null) {
const book = books.find((item) => item.id === id);
if (!book) {
sendError(response, 404, 'BOOK_NOT_FOUND', 'Книгу не знайдено');
return;
}
sendJson(response, 200, book);
return;
}
if (request.method === 'POST' && id === null) {
const data = await readJsonBody(request);
if (
typeof data.title !== 'string' ||
data.title.trim() === '' ||
typeof data.author !== 'string' ||
data.author.trim() === ''
) {
sendError(
response,
400,
'VALIDATION_ERROR',
'Поля title та author є обов’язковими'
);
return;
}
const book = {
id: nextId++,
title: data.title.trim(),
author: data.author.trim(),
};
books.push(book);
sendJson(response, 201, book, {
Location: `/api/v1/books/${book.id}`,
});
return;
}
if (request.method === 'PATCH' && id !== null) {
const book = books.find((item) => item.id === id);
if (!book) {
sendError(response, 404, 'BOOK_NOT_FOUND', 'Книгу не знайдено');
return;
}
const data = await readJsonBody(request);
if ('title' in data) {
if (typeof data.title !== 'string' || data.title.trim() === '') {
sendError(
response,
400,
'VALIDATION_ERROR',
'Поле title має бути непорожнім рядком'
);
return;
}
book.title = data.title.trim();
}
if ('author' in data) {
if (typeof data.author !== 'string' || data.author.trim() === '') {
sendError(
response,
400,
'VALIDATION_ERROR',
'Поле author має бути непорожнім рядком'
);
return;
}
book.author = data.author.trim();
}
sendJson(response, 200, book);
return;
}
if (request.method === 'DELETE' && id !== null) {
const bookIndex = books.findIndex((item) => item.id === id);
if (bookIndex === -1) {
sendError(response, 404, 'BOOK_NOT_FOUND', 'Книгу не знайдено');
return;
}
books.splice(bookIndex, 1);
response.writeHead(204);
response.end();
return;
}
sendError(
response,
405,
'METHOD_NOT_ALLOWED',
'Метод не підтримується для цього URI'
);
} catch (error) {
if (error.message === 'INVALID_JSON') {
sendError(response, 400, 'INVALID_JSON', 'Тіло запиту має бути коректним JSON');
return;
}
if (error.message === 'REQUEST_TOO_LARGE') {
sendError(response, 413, 'REQUEST_TOO_LARGE', 'Тіло запиту занадто велике');
return;
}
console.error(error);
sendError(response, 500, 'INTERNAL_ERROR', 'Внутрішня помилка сервера');
}
});
server.listen(3000, () => {
console.log('REST API працює на http://localhost:3000');
});Запустіть сервер:
node server.jsОтримати всі книги:
curl http://localhost:3000/api/v1/booksОтримати одну книгу:
curl http://localhost:3000/api/v1/books/1Створити книгу:
curl -X POST http://localhost:3000/api/v1/books \
-H "Content-Type: application/json" \
-d '{"title":"Місто","author":"Валер’ян Підмогильний"}'Частково оновити книгу:
curl -X PATCH http://localhost:3000/api/v1/books/1 \
-H "Content-Type: application/json" \
-d '{"title":"Новий заголовок"}'Видалити книгу:
curl -X DELETE http://localhost:3000/api/v1/books/1У прикладі:
/api/v1/books представляє колекцію книг;
/api/v1/books/:id представляє окрему книгу;
HTTP-метод визначає операцію;
тіло запиту використовується для створення та оновлення;
статус-код показує результат;
помилки мають єдиний формат.
Перед реалізацією кожного endpoint перевірте:
Який ресурс він представляє?
Чи описує URI ресурс, а не дію?
Чи правильно вибрано HTTP-метод?
Який статус-код повертається у разі успіху?
Що отримає клієнт, якщо ресурс не знайдено?
Як виглядає помилка валідації?
Чи однаково оформлені подібні відповіді?
Чи зрозуміло клієнту, які поля потрібні?
Наприклад, для створення книги:
Ресурс: книга
URI: /api/v1/books
Метод: POST
Тіло: title, author
Успіх: 201 Created
Помилка неправильних даних: 400 Bad RequestПогано:
POST /api/v1/createBook
POST /api/v1/deleteBook/1Краще:
POST /api/v1/books
DELETE /api/v1/books/1Якщо endpoint лише отримує дані, не потрібно використовувати POST замість GET. Метод повинен відповідати призначенню операції.
200 OK для всіх випадківСтатус 200 не підходить для кожної ситуації:
створення ресурсу — 201;
успішне видалення без тіла — 204;
ресурс не знайдено — 404;
неправильні дані — 400.
Коректні статус-коди спрощують обробку API на клієнті.
Погано, коли один endpoint повертає:
{
"message": "Помилка"
}а інший:
{
"errorMessage": "Помилка"
}Варто обрати один формат і використовувати його всюди.
PUT і PATCHPUT призначений для повної заміни ресурсу, а PATCH — для часткової зміни. Якщо змінюється лише одне поле, доречнішим буде PATCH.
Дані від клієнта не можна вважати коректними автоматично. Сервер має перевіряти:
наявність обов’язкових полів;
типи значень;
допустимі значення;
формат ідентифікаторів.
REST API організовує взаємодію клієнта із ресурсами.
URI повинні описувати ресурси, а не дії.
Колекція та окремий ресурс мають різні URI.
GET отримує дані, POST створює ресурс, PUT повністю замінює, PATCH частково оновлює, DELETE видаляє.
Статус-коди повідомляють результат операції.
JSON і єдиний формат помилок роблять API передбачуваним.
Узгоджені назви, методи та відповіді спрощують використання API клієнтами.