Пошук уроків, статей та іншого контенту
Розгляне принципи REST, HTTP-методи, статус-коди, ресурсну модель і проєктування надійних API.
REST (Representational State Transfer) — це підхід до проєктування вебсервісів. REST API дає змогу клієнтам взаємодіяти з даними через HTTP-запити.
Наприклад, застосунок зі списком завдань може надавати такі ресурси:
GET /tasks — отримати список завдань;
GET /tasks/42 — отримати завдання з ідентифікатором 42;
POST /tasks — створити нове завдання;
PATCH /tasks/42 — змінити завдання;
DELETE /tasks/42 — видалити завдання.
REST API працює з ресурсами, а не з діями. Ресурсом може бути користувач, товар, замовлення, стаття або завдання.
Кожен тип даних представляється ресурсом і має URL.
Приклади:
/users
/users/15
/products
/products/8
/orders/102Зазвичай:
URL у множині позначає колекцію ресурсів;
URL з ідентифікатором позначає один ресурс.
/tasks // колекція завдань
/tasks/42 // конкретне завданняНе варто будувати URL як назви команд:
/getTasks
/createTask
/deleteTask/42HTTP-метод уже описує операцію, тому краще використовувати:
GET /tasks
POST /tasks
DELETE /tasks/42Клієнт і сервер відповідають за різні частини системи:
клієнт відображає інтерфейс і надсилає запити;
сервер зберігає дані, застосовує бізнес-правила та формує відповіді.
Завдяки цьому клієнт і сервер можна розробляти та змінювати незалежно.
REST-запит має містити всю інформацію, необхідну для його обробки. Сервер не повинен покладатися на пам’ять про попередній запит клієнта.
Наприклад, якщо API використовує токен авторизації, клієнт надсилає його з кожним запитом:
Authorization: Bearer token-valueСервер не повинен припускати, що користувач уже виконував певний запит раніше.
Однакові правила роблять API передбачуваним:
ресурси мають зрозумілі URL;
HTTP-методи використовуються за призначенням;
відповіді містять відповідні статус-коди;
формат даних узгоджений, наприклад JSON.
Клієнт отримує не сам ресурс у внутрішньому вигляді сервера, а його представлення. Найчастіше це JSON.
Наприклад, внутрішній об’єкт завдання може бути представлений так:
{
"id": 42,
"title": "Вивчити REST",
"completed": false
}GET отримує ресурс або колекцію ресурсів.
GET /tasks
GET /tasks/42Особливості:
не повинен змінювати дані;
може повертати список або один ресурс;
зазвичай має статус 200 OK.
Приклад відповіді:
[
{
"id": 1,
"title": "Прочитати документацію",
"completed": true
},
{
"id": 2,
"title": "Створити API",
"completed": false
}
]POST створює новий ресурс у колекції.
POST /tasks
Content-Type: application/json
{
"title": "Вивчити статус-коди"
}Якщо ресурс створено, сервер зазвичай повертає 201 Created і створений об’єкт:
{
"id": 3,
"title": "Вивчити статус-коди",
"completed": false
}PUT замінює ресурс повністю.
PUT /tasks/42
Content-Type: application/json
{
"title": "Вивчити REST API",
"completed": true
}Якщо клієнт не передав поле, сервер може сприйняти це як бажання видалити його або повернути помилку. Тому PUT варто використовувати, коли клієнт передає повне представлення ресурсу.
PATCH змінює лише окремі поля ресурсу.
PATCH /tasks/42
Content-Type: application/json
{
"completed": true
}Це зручно, коли потрібно оновити лише одну властивість.
DELETE видаляє ресурс.
DELETE /tasks/42Успішна відповідь може мати статус:
204 No Content, якщо тіло відповіді не потрібне;
200 OK, якщо сервер повертає додаткову інформацію.
Статус-код показує результат обробки запиту.
200 OK — запит успішно виконано;
201 Created — ресурс створено;
202 Accepted — запит прийнято для подальшої обробки;
204 No Content — запит успішний, але тіло відповіді відсутнє.
400 Bad Request — некоректний запит;
401 Unauthorized — користувач не автентифікований;
403 Forbidden — користувач автентифікований, але не має дозволу;
404 Not Found — ресурс не знайдено;
409 Conflict — конфлікт із поточним станом даних;
422 Unprocessable Content — дані мають неправильний формат або не проходять перевірку;
429 Too Many Requests — клієнт надсилає забагато запитів.
500 Internal Server Error — неочікувана помилка на сервері;
503 Service Unavailable — сервіс тимчасово недоступний.
Клієнт може використовувати статус-код без аналізу тексту повідомлення:
if (response.status === 404) {
// Ресурс не знайдено
}Помилки краще повертати в однаковому форматі. Наприклад:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Поле title є обов’язковим",
"details": {
"field": "title"
}
}
}Єдиний формат спрощує обробку помилок на клієнті.
Не варто повертати внутрішні деталі сервера, наприклад трасування винятку або паролі з’єднання з базою даних.
Нижче наведено мінімальний API для роботи із завданнями. Він використовує вбудований модуль http, тому додаткові бібліотеки не потрібні.
Збережіть код у файлі server.js і запустіть командою:
node server.jsconst http = require("http");
const tasks = [
{ id: 1, title: "Вивчити HTTP-методи", completed: true },
{ id: 2, title: "Створити REST API", completed: false }
];
let nextId = 3;
function sendJson(response, statusCode, data) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8"
});
if (statusCode === 204) {
response.end();
return;
}
response.end(JSON.stringify(data));
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
let body = "";
request.on("data", (chunk) => {
body += chunk;
// Обмежуємо розмір тіла запиту
if (body.length > 1_000_000) {
reject(new Error("Request body is too large"));
request.destroy();
}
});
request.on("end", () => {
if (!body) {
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}`);
const pathParts = url.pathname.split("/").filter(Boolean);
// GET /tasks — повертаємо всі завдання
if (request.method === "GET" && pathParts.length === 1 && pathParts[0] === "tasks") {
sendJson(response, 200, tasks);
return;
}
// GET /tasks/:id — повертаємо одне завдання
if (request.method === "GET" && pathParts.length === 2 && pathParts[0] === "tasks") {
const id = Number(pathParts[1]);
const task = tasks.find((item) => item.id === id);
if (!task) {
sendJson(response, 404, {
error: {
code: "TASK_NOT_FOUND",
message: "Завдання не знайдено"
}
});
return;
}
sendJson(response, 200, task);
return;
}
// POST /tasks — створюємо завдання
if (request.method === "POST" && pathParts.length === 1 && pathParts[0] === "tasks") {
try {
const data = await readJsonBody(request);
if (typeof data.title !== "string" || data.title.trim() === "") {
sendJson(response, 422, {
error: {
code: "VALIDATION_ERROR",
message: "Поле title є обов’язковим"
}
});
return;
}
const task = {
id: nextId++,
title: data.title.trim(),
completed: false
};
tasks.push(task);
sendJson(response, 201, task);
} catch {
sendJson(response, 400, {
error: {
code: "INVALID_JSON",
message: "Тіло запиту має містити коректний JSON"
}
});
}
return;
}
// DELETE /tasks/:id — видаляємо завдання
if (request.method === "DELETE" && pathParts.length === 2 && pathParts[0] === "tasks") {
const id = Number(pathParts[1]);
const taskIndex = tasks.findIndex((item) => item.id === id);
if (taskIndex === -1) {
sendJson(response, 404, {
error: {
code: "TASK_NOT_FOUND",
message: "Завдання не знайдено"
}
});
return;
}
tasks.splice(taskIndex, 1);
sendJson(response, 204);
return;
}
sendJson(response, 404, {
error: {
code: "ROUTE_NOT_FOUND",
message: "Маршрут не знайдено"
}
});
});
server.listen(3000, () => {
console.log("API запущено на http://localhost:3000");
});Приклади запитів до цього API:
curl http://localhost:3000/taskscurl http://localhost:3000/tasks/1curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Перевірити API"}'curl -X DELETE http://localhost:3000/tasks/1У прикладі масив tasks зберігається лише в пам’яті. Після перезапуску сервера дані повернуться до початкового стану.
Під час створення API дотримуйтеся простих і послідовних правил.
GET /users
GET /users/15
GET /users/15/ordersЗамість:
GET /getUsers
POST /createUserЯкщо замовлення належать конкретному користувачу, можна використати:
GET /users/15/ordersАле надто глибока вкладеність ускладнює API:
/users/15/orders/8/items/3/reviewsДля складних випадків краще надати окремий зрозумілий ресурс:
/reviews/3Параметри запиту підходять для фільтрації, пошуку, сортування та пагінації:
GET /tasks?completed=false
GET /products?category=books
GET /users?sort=name
GET /articles?page=2&limit=20Шлях визначає ресурс, а параметри уточнюють, які саме дані потрібно отримати.
Надійність API означає, що воно передбачувано працює для коректних і некоректних запитів.
Сервер не повинен довіряти даним клієнта. Перевіряйте:
наявність обов’язкових полів;
типи значень;
допустимий діапазон;
довжину рядків;
взаємозалежність полів.
Наприклад, для створення завдання потрібно перевірити, що title є непорожнім рядком.
Не використовуйте 200 OK для всіх ситуацій.
Наприклад:
відсутній ресурс — 404;
неправильні дані — 422;
успішне створення — 201;
успішне видалення без відповіді — 204.
Це дає змогу клієнту правильно реагувати на результат.
Ідемпотентна операція дає той самий результат після повторного виконання.
Зазвичай:
GET є ідемпотентним;
PUT є ідемпотентним;
DELETE є ідемпотентним;
POST зазвичай не є ідемпотентним.
Повторний POST /tasks може створити два завдання. Повторний DELETE /tasks/42 не повинен видалити щось додатково після першого видалення.
Мережеві помилки можуть змусити клієнта повторити запит. Для операцій, які не мають створювати дублікати, застосовують ідемпотентні ключі або інші механізми контролю повторів.
На базовому рівні важливо хоча б розуміти різницю:
POST /paymentsможе повторно створити платіж, якщо клієнт не отримав відповідь і надіслав запит ще раз.
Публічний API має описувати ресурси й правила роботи з ними, а не структуру таблиць у базі даних.
Наприклад, клієнту не обов’язково знати, що поле user_id зберігається в конкретній таблиці. API може повертати зрозуміле представлення:
{
"id": 42,
"author": {
"id": 7,
"name": "Олена"
}
}Коли несумісні зміни неможливо уникнути, API версіюють. Один із простих варіантів — додати версію до URL:
/api/v1/tasks
/api/v2/tasksВерсіювання дає змогу старим клієнтам продовжувати працювати зі старою структурою відповідей.
Версію потрібно змінювати не для кожного виправлення, а тоді, коли нова поведінка несумісна зі старою.
Погано:
POST /tasks/42/deleteКраще:
DELETE /tasks/42200 OK для помилокПогано:
HTTP/1.1 200 OK
{
"error": "Task not found"
}Краще:
HTTP/1.1 404 Not Found
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "Завдання не знайдено"
}
}Рядок /tasks/abc не повинен автоматично перетворюватися на некоректний ідентифікатор. Сервер має перевірити параметр і повернути зрозумілу помилку.
Якщо в одному ресурсі використовується createdAt, а в іншому — created_at без чіткої причини, клієнтам складніше працювати з API. Оберіть єдиний стиль і дотримуйтеся його.
Не слід повертати всі поля та всі пов’язані ресурси в кожній відповіді. Для великих колекцій використовуйте фільтрацію, пагінацію та обмеження кількості результатів.
REST API працює з ресурсами, представленими URL.
HTTP-метод визначає операцію над ресурсом.
GET отримує дані, POST створює, PUT замінює, PATCH частково оновлює, DELETE видаляє.
Статус-коди описують результат запиту.
Помилки мають повертатися в передбачуваному форматі.
Сервер повинен перевіряти вхідні дані та не розкривати внутрішні деталі.
Надійне API використовує послідовні URL, правильні статуси та враховує повторне виконання запитів.