Як побудувати REST API з Express.js?
REST API з Express.js - сервер, який призначає кожному HTTP-методу і шляху обробник, що читає, створює, оновлює або видаляє ресурс.
Теорія
TL;DR
app.use(express.json())потрібно додати до маршрутів, що читаютьreq.body, інакшеreq.bodyбудеundefined- GET читає, POST створює, PUT замінює цілий об'єкт, PATCH оновлює окремі поля, DELETE видаляє
- Коди статусу - частина контракту: 201 для створення, 204 для видалення (без тіла), 404 якщо не знайдено, 400 при невалідних даних
- Параметри маршруту в
req.params, рядок запиту вreq.query, тіло запиту вreq.body - Імена ресурсів у URL мають бути іменниками у множині:
/users, не/userі не/getUsers
Мінімальний робочий приклад
const express = require('express');
const app = express();
app.use(express.json()); // без цього req.body завжди undefined
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]); // повертає масив JSON
});
app.post('/users', (req, res) => {
const { name } = req.body; // читає розпарсений JSON
res.status(201).json({ id: 2, name }); // 201 = Created
});
app.listen(3000);express.json() парсить тіло запиту як JSON і кладе результат у req.body. Без цього рядка обробники POST і PUT не отримують нічого.
HTTP-методи і CRUD
| HTTP-метод | CRUD | Коли використовувати |
|---|---|---|
| GET | Читати | Отримати ресурс або список |
| POST | Створити | Створити новий ресурс |
| PUT | Оновити (повністю) | Замінити весь об'єкт |
| PATCH | Оновити (частково) | Змінити одне або кілька полів |
| DELETE | Видалити | Видалити ресурс |
PUT замінює весь об'єкт. Якщо ти PUT-нув { name: 'Alice' } до користувача, у якого був ще й email, email зникне. PATCH чіпає лише те, що ти надіслав. Більшість реальних API використовують PATCH для оновлень, бо клієнту рідко потрібно надсилати незмінені дані повторно.
Читання даних запиту
// GET /users/42?format=json
app.get('/users/:id', (req, res) => {
req.params.id // '42' — завжди рядок
req.query.format // 'json'
req.headers['authorization'] // Bearer-токен
req.method // 'GET'
req.path // '/users/42'
});
// POST /users з JSON-тілом
app.post('/users', (req, res) => {
req.body.name // з розпарсеного JSON
req.body.email
});req.params.id завжди рядок. Порівнювати його з числовим ID через === не вийде: вони ніколи не збіжаться. Конвертуй через Number(req.params.id) перед пошуком.
Надсилання відповідей
res.json({ data }) // надсилає JSON, Content-Type встановлюється автоматично
res.status(201).json(data) // статус + тіло по ланцюжку
res.status(204).send() // без тіла — для успішного DELETE
res.status(404).json({ error: 'Не знайдено' })
res.redirect('/new-path')Express за замовчуванням повертає 200. Клієнт, що отримав 200 на POST, не знає, чи ресурс справді створили. Встановлюй код статусу явно.
Правила проектування
- Іменники в URL, не дієслова:
/users, не/getUsersабо/deleteUser - Множина для назв ресурсів:
/users, не/user - Версіонування з першого дня:
/api/v1/users - Єдина структура JSON: однаковий формат для успіху і помилки в кожній відповіді
- HTTP-методи несуть дію, не шляхи URL
Версіонування важливіше, ніж здається. Коли ти задеплоїв /api/users і клієнт почав ним користуватись, змінити структуру відповіді вже не вийде без зламу. /api/v2/users дає змогу розвивати API, поки v1 залишається стабільним.
Типові помилки
Відсутній express.json()
// Неправильно — req.body undefined, помилки немає
app.post('/users', (req, res) => {
const user = { ...req.body }; // {} — порожній об'єкт, дані втрачено
users.push(user);
res.status(201).json(user);
});
// Правильно
app.use(express.json()); // додай перед визначенням маршрутівНеправильні коди статусу
// Неправильно — 200 при створенні, 200 при видаленні
app.post('/users', (req, res) => res.json(user));
app.delete('/users/:id', (req, res) => res.json({ ok: true }));
// Правильно
app.post('/users', (req, res) => res.status(201).json(user));
app.delete('/users/:id', (req, res) => res.status(204).send());Забути конвертувати req.params.id
// Неправильно — рядок '42' ніколи не дорівнює числу 42
const user = users.find(u => u.id === req.params.id);
// Правильно
const user = users.find(u => u.id === Number(req.params.id));Дієслова в URL
// Неправильно
app.post('/createUser', ...);
app.get('/deleteUser/:id', ...);
// Правильно
app.post('/users', ...);
app.delete('/users/:id', ...);Де зустрічається на практиці
- Express - HTTP-шар у багатьох Node.js-бекендах, часто разом з ORM для бази даних
- В реальних проектах маршрути розбивають по файлах через
express.Router() express.urlencoded()обробляє HTML-форми,express.json()- API-клієнти- Валідація відбувається до звернення до бази: перевіряй обов'язкові поля або використовуй бібліотеки типу
zodчиjoi
Питання на співбесіді
Q: Яка різниця між PUT і PATCH?
A: PUT замінює весь об'єкт. Поля, які не надіслав, зникають. PATCH оновлює лише ті поля, що є в тілі запиту. Більшість команд використовують PATCH для оновлень, щоб не видалити дані випадково.
Q: Чому повертати 204 замість 200 при DELETE?
A: 204 означає успіх без тіла відповіді. Ресурс більше не існує, повертати нічого. 200 з порожнім тілом теж спрацює, але 204 - прийнята конвенція.
Q: Що відбувається, якщо два маршрути збігаються з одним шляхом?
A: Express виконає перший збіг і зупиниться. Другий не запуститься, якщо перший обробник не викличе next(). Це важливо при додаванні middleware поверх обробників маршрутів.
Q: Як розбити маршрути по файлах при зростанні додатка?
A: Використовуй express.Router(). Визнач маршрути на екземплярі роутера, експортуй його, потім app.use('/users', usersRouter) у головному файлі. Роутер працює з '/' і '/:id', префікс задається через app.use.
Q: Як захистити маршрут для автентифікованих користувачів?
A: Напиши middleware-функцію, що перевіряє заголовок Authorization, валідує токен і або викликає next(), або повертає res.status(401).json({ error: 'Unauthorized' }). Додай її перед обробником: app.get('/users', authMiddleware, handler).
Приклади
Повний CRUD для ресурсу users
const express = require('express');
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextId = 3;
// 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 === Number(req.params.id));
if (!user) return res.status(404).json({ error: 'Користувача не знайдено' });
res.json(user);
});
// POST /users
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'name та email є обовязковими' });
}
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
// PUT /users/:id
app.put('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Користувача не знайдено' });
users[index] = { id: Number(req.params.id), ...req.body };
res.json(users[index]);
});
// PATCH /users/:id
app.patch('/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: 'Користувача не знайдено' });
Object.assign(user, req.body);
res.json(user);
});
// DELETE /users/:id
app.delete('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Користувача не знайдено' });
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000, () => console.log('API працює на порту 3000'));Кожен обробник повертає раннє 404 при відсутності ресурсу. Це тримає основний шлях плоским і позбавляє від вкладених if. Варто зробити це стандартом з першого маршруту.
Єдина структура відповіді
// Успіх
res.json({ success: true, data: user });
// Помилка валідації
res.status(400).json({ success: false, error: 'name та email є обовязковими' });
// Список з пагінацією
res.json({
success: true,
data: users,
pagination: { page: 1, limit: 10, total: 100 }
});Клієнт перевіряє success, потім читає data або error. Єдиний формат означає одне місце в клієнтському коді для обробки всіх відповідей.
Розбивка маршрутів через express.Router
// routes/users.js
const router = require('express').Router();
router.get('/', (req, res) => res.json(users));
router.get('/:id', (req, res) => { /* один користувач */ });
router.post('/', (req, res) => { /* створення */ });
module.exports = router;
// app.js
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);
// GET /users -> router.get('/')
// GET /users/1 -> router.get('/:id')Файл роутера не знає свого префіксу. app.use('/users', usersRouter) задає його при підключенні. Тому всередині роутера пишеш / і /:id, а не /users і /users/:id.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.