Пошук уроків, статей та іншого контенту
Створите маршрути, орієнтовані на ресурси, і зв’яжете їх із контролерами Node.js без дублювання логіки.
Маршрутизація визначає, як застосунок реагує на HTTP-запити. У ресурсно-орієнтованому підході URL описує ресурс, а HTTP-метод — дію над ним.
Наприклад, якщо застосунок працює з книгами, ресурсом буде books:
| Метод | URL | Призначення | |---|---|---| | GET | /books | отримати список книг | | GET | /books/:id | отримати одну книгу | | POST | /books | створити книгу | | PATCH | /books/:id | частково оновити книгу | | DELETE | /books/:id | видалити книгу |
Назва ресурсу зазвичай є іменником у множині. Маршрут /books означає колекцію книг, а /books/:id — окрему книгу з певним ідентифікатором.
Такий підхід допомагає:
зробити API передбачуваним;
не створювати окремий URL для кожної дії;
використовувати HTTP-методи за їхнім призначенням;
розділити маршрутизацію та бізнес-логіку.
Маршрут має відповідати на запитання: який URL і HTTP-метод обробляються?
Наприклад, маршрут може лише зв’язати GET /books із функцією listBooks:
router.get("/", listBooks);А отримання та формування відповіді відбувається в контролері:
function listBooks(req, res) {
res.json(books);
}Це краще, ніж розміщувати всю логіку безпосередньо в оголошенні маршруту:
// Такий підхід швидко створює дублювання та великі обробники
router.get("/books", (req, res) => {
// пошук книг
// перевірка даних
// формування відповіді
});Для невеликого прикладу всю логіку можна зберігати в одному файлі, але навіть тоді варто винести обробники запитів в окремі функції.
Для прикладу створимо API книг на Express. Дані зберігатимуться в масиві, тому після перезапуску сервера вони втратяться.
Встановіть залежність:
npm init -y
npm install expressСтворіть файл app.js:
const express = require("express");
const app = express();
const port = 3000;
app.use(express.json());
let nextId = 3;
const books = [
{
id: 1,
title: "Кобзар",
author: "Тарас Шевченко"
},
{
id: 2,
title: "Лісова пісня",
author: "Леся Українка"
}
];
function findBookById(id) {
return books.find((book) => book.id === Number(id));
}
// Контролер для отримання всіх книг
function listBooks(req, res) {
res.json(books);
}
// Контролер для отримання однієї книги
function getBook(req, res) {
const book = findBookById(req.params.id);
if (!book) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}
res.json(book);
}
// Контролер для створення книги
function createBook(req, res) {
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({
message: "Поля title та author є обов'язковими"
});
}
const book = {
id: nextId++,
title,
author
};
books.push(book);
res.status(201).json(book);
}
// Контролер для часткового оновлення книги
function updateBook(req, res) {
const book = findBookById(req.params.id);
if (!book) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}
const { title, author } = req.body;
if (title !== undefined) {
book.title = title;
}
if (author !== undefined) {
book.author = author;
}
res.json(book);
}
// Контролер для видалення книги
function deleteBook(req, res) {
const bookIndex = books.findIndex(
(book) => book.id === Number(req.params.id)
);
if (bookIndex === -1) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}
books.splice(bookIndex, 1);
res.status(204).send();
}
// Маршрути ресурсу books
app.get("/books", listBooks);
app.get("/books/:id", getBook);
app.post("/books", createBook);
app.patch("/books/:id", updateBook);
app.delete("/books/:id", deleteBook);
app.listen(port, () => {
console.log(`Сервер запущено на http://localhost:${port}`);
});Запустіть сервер:
node app.jsПісля цього доступні такі операції:
curl http://localhost:3000/bookscurl http://localhost:3000/books/1curl -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-d '{"title":"Тигролови","author":"Іван Багряний"}'curl -X PATCH http://localhost:3000/books/1 \
-H "Content-Type: application/json" \
-d '{"author":"Тарас Григорович Шевченко"}'curl -X DELETE http://localhost:3000/books/1У маршруті /books/:id частина :id є параметром маршруту. Express передає його в req.params.
app.get("/books/:id", (req, res) => {
const id = req.params.id;
res.json({
requestedId: id
});
});Якщо клієнт надішле запит на /books/15, значення req.params.id буде рядком "15".
Тому під час порівняння з числовим ідентифікатором його потрібно перетворити:
const bookId = Number(req.params.id);У прикладі використовується допоміжна функція:
function findBookById(id) {
return books.find((book) => book.id === Number(id));
}Так перетворення виконується в одному місці, а не дублюється в кожному контролері.
Контролери повинні повертати статус, який описує результат операції.
Найчастіше для ресурсів використовуються такі статуси:
200 OK — запит успішно виконано;
201 Created — ресурс успішно створено;
204 No Content — операцію виконано, але тіло відповіді відсутнє;
400 Bad Request — дані запиту некоректні або неповні;
404 Not Found — ресурс не знайдено.
Наприклад, після створення книги потрібно повернути 201:
res.status(201).json(book);Після успішного видалення можна повернути 204 без тіла:
res.status(204).send();Якщо книга не існує, контролер має завершити виконання після відправлення помилки:
if (!book) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}return запобігає подальшому виконанню функції та повторній спробі надіслати відповідь.
Коли маршрутів стає більше, їх можна об’єднати в express.Router. Це дозволяє зберігати маршрути конкретного ресурсу окремо від запуску сервера.
Файл routes/books.js:
const express = require("express");
const router = express.Router();
const books = [
{
id: 1,
title: "Кобзар",
author: "Тарас Шевченко"
}
];
router.get("/", (req, res) => {
res.json(books);
});
router.get("/:id", (req, res) => {
const book = books.find(
(item) => item.id === Number(req.params.id)
);
if (!book) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}
res.json(book);
});
module.exports = router;Підключення роутера в app.js:
const express = require("express");
const booksRouter = require("./routes/books");
const app = express();
app.use(express.json());
app.use("/books", booksRouter);
app.listen(3000, () => {
console.log("Сервер запущено на http://localhost:3000");
});У результаті:
router.get("/") обробляє GET /books;
router.get("/:id") обробляє GET /books/:id.
Префікс /books додається під час підключення роутера, тому в самому роутері не потрібно повторювати його.
Дублювання виникає, коли одна й та сама операція реалізована в кількох маршрутах або контролерах.
Наприклад, пошук книги за ідентифікатором потрібен і для отримання, і для оновлення. Замість повторення коду використовуйте спільну функцію:
function findBookById(id) {
return books.find((book) => book.id === Number(id));
}Маршрути також не повинні містити різні варіанти одного й того самого ресурсу:
// Невдалий варіант
app.get("/get-books", listBooks);
app.get("/get-book/:id", getBook);
app.post("/create-book", createBook);Краще використовувати один ресурс і різні HTTP-методи:
app.get("/books", listBooks);
app.get("/books/:id", getBook);
app.post("/books", createBook);У результаті URL описує ресурс, а метод описує операцію над ним.
Для оновлення ресурсу можуть використовуватися PUT і PATCH.
PUT зазвичай означає повну заміну ресурсу;
PATCH означає часткове оновлення.
У прикладі використано PATCH, тому клієнт може передати лише одне поле:
{
"title": "Нова назва"
}Інші поля залишаться без змін.
Якщо API очікує повний об’єкт під час оновлення, можна використовувати PUT. Головне — послідовно дотримуватися обраної поведінки в усьому застосунку.
Маршрути на кшталт /books/create або /books/delete/:id ускладнюють структуру API.
Краще:
app.post("/books", createBook);
app.delete("/books/:id", deleteBook);Маршрут /books працює з усією колекцією. Для конкретної книги потрібен параметр:
app.get("/books/:id", getBook);Не слід одразу додавати req.body до масиву. Спочатку перевірте обов’язкові поля:
if (!title || !author) {
return res.status(400).json({
message: "Поля title та author є обов'язковими"
});
}Після res.status(...).json(...) контролер повинен завершити виконання. Для цього використовуйте return:
if (!book) {
return res.status(404).json({
message: "Книгу не знайдено"
});
}Якщо роутер підключено так:
app.use("/books", booksRouter);у ньому не потрібно писати /books ще раз:
router.get("/", listBooks);а не:
router.get("/books", listBooks);Ресурсно-орієнтований маршрут описує ресурс, наприклад /books.
HTTP-метод визначає дію: отримання, створення, оновлення або видалення.
Колекція використовує URL /books, а окремий елемент — /books/:id.
Контролери містять логіку обробки запитів, а маршрути лише зв’язують URL із контролерами.
Спільні операції, наприклад пошук за ідентифікатором, потрібно виносити в окремі функції.
Для масштабування маршрути конкретного ресурсу зручно об’єднувати в express.Router.
Коректні HTTP-статуси та перевірка вхідних даних роблять API зрозумілим і надійним.