REST API на Express
REST API (Representational State Transfer) — архитектурный стиль HTTP API, где ресурсы (users, products) идентифицируются URL, а действия над ними — HTTP-методами (GET, POST, PUT, DELETE).
Зачем нужно
REST — стандарт де-факто для веб-API: предсказуемые URL, статус-коды и методы упрощают интеграцию с фронтендом, мобильными приложениями и сторонними сервисами. Express предоставляет минимальный инструментарий для построения REST API без лишних абстракций.
Где используется
- Бэкенд для SPA (React, Vue, Angular)
- Мобильные приложения (iOS, Android)
- Публичные API для внешних разработчиков
- Микросервисная коммуникация
Основной контент
RESTful соглашения
GET /api/users — список пользователей
GET /api/users/:id — один пользователь
POST /api/users — создать пользователя
PUT /api/users/:id — полная замена
PATCH /api/users/:id — частичное обновление
DELETE /api/users/:id — удалить
HTTP-статусы:
200 OK — успешный GET, PUT, PATCH
201 Created — успешный POST
204 No Content — успешный DELETE
400 Bad Request — невалидные данные
401 Unauthorized — не авторизован
403 Forbidden — нет прав
404 Not Found — ресурс не найден
409 Conflict — конфликт (дубликат)
500 Internal Server Error — ошибка сервера
Полный пример REST API для ресурса Users
// routes/users.js
const express = require('express');
const router = express.Router;
const UsersService = require('../services/UsersService');
// GET /api/users?page=1&limit=20&search=alice
router.get('/', async (req, res, next) => {
try {
const { page = 1, limit = 20, search = '' } = req.query;
const users = await UsersService.getAll({ page: +page, limit: +limit, search });
res.json({
data: users,
meta: { page: +page, limit: +limit, total: users.total }
});
} catch (err) { next(err); }
});
// GET /api/users/:id
router.get('/:id', async (req, res, next) => {
try {
const user = await UsersService.getById(req.params.id);
res.json(user);
} catch (err) { next(err); }
});
// POST /api/users
router.post('/', async (req, res, next) => {
try {
const user = await UsersService.create(req.body);
res.status(201).json(user);
} catch (err) { next(err); }
});
// PATCH /api/users/:id
router.patch('/:id', async (req, res, next) => {
try {
const user = await UsersService.update(req.params.id, req.body);
res.json(user);
} catch (err) { next(err); }
});
// DELETE /api/users/:id
router.delete('/:id', async (req, res, next) => {
try {
await UsersService.delete(req.params.id);
res.status(204).send;
} catch (err) { next(err); }
});
module.exports = router;
Валидация входных данных
npm install joi
const Joi = require('joi');
const createUserSchema = Joi.object({
name: Joi.string.min(2).max(50).required,
email: Joi.string.email.required,
age: Joi.number.integer.min(18).max(120)
});
// Middleware для валидации
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json({
error: 'ValidationError',
details: error.details.map(d => d.message)
});
}
req.body = value; // нормализованные данные
next;
};
}
router.post('/', validate(createUserSchema), async (req, res, next) => {
// req.body уже валиден
});
Форматирование ответов
// Единый формат успешного ответа
res.json({
data: user,
meta: { requestId: req.id, timestamp: new Date.toISOString() }
});
// Единый формат ошибки (из error middleware)
res.status(err.status || 500).json({
error: err.name || 'Error',
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
Пагинация и фильтрация
// GET /api/products?page=2&limit=10&category=electronics&sort=price&order=asc
router.get('/', async (req, res, next) => {
const { page = 1, limit = 10, category, sort = 'id', order = 'asc' } = req.query;
const offset = (page - 1) * limit;
const [items, total] = await Promise.all([
ProductRepository.findAll({ category, sort, order, limit: +limit, offset }),
ProductRepository.count({ category })
]);
res.json({
data: items,
pagination: {
page: +page, limit: +limit, total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total
}
});
});
Частые ошибки
- PUT вместо PATCH — PUT заменяет весь ресурс, PATCH — только переданные поля; не путать
- Возвращать данные при DELETE — REST-соглашение: DELETE возвращает 204 No Content без тела
- Не версионировать API —
/api/v1/usersпозволяет выпускать/api/v2/usersбез ломающих изменений - Хранить состояние на сервере — REST — stateless; авторизацию передавать в каждом запросе (JWT, API key)
Связанные темы
- _MOC Node.js
- Express -- маршруты и методы
- Express -- Router и модуляризация
- Layered Architecture
- HTTP-методы -- GET, POST, PUT, DELETE, PATCH
- HTTP-статусы -- 1xx, 2xx, 3xx, 4xx, 5xx
Ресурсы
🎓 Источник: Разработка API на Node.js
- 📅 2019-03-28 · YouTube · [Marp](../../Documents/TimurShemsedinov/2019-03-28 — Разработка API на Node.js (клиент и сервер) (-az912XBCu8).md)
- Тезисы (взгляд против REST):
- API без привязки к транспорту: одна и та же функция должна работать по HTTP, WebSocket, IPC — без переписывания
- Все экспортируемые функции async — единый контракт
- Чистая бизнес-логика без сети — отдельно от транспортного слоя
- Клиентская функция = представитель серверной:
await api.getUser(id)на клиенте, такой же на сервере throwиз async уходит в reject — единая обработка ошибок- API крошечное: 5 функций, 1.7 KB — не нужно громоздких REST-роутеров
- REST не нужен в Node: долгоживущий процесс держит сессию, RPC удобнее
- Endpoint со списком функций — клиент сам подгружает proxy
- Цитата: «В Node не нужен REST — у тебя одинаковый язык на клиенте и сервере, RPC естественнее»
🎓 Источник: Разработка API на Node.js и Metarhia
- 📅 2021-01-18 · YouTube · [Marp](../../Documents/TimurShemsedinov/2021-01-18 — 💻 Разработка API на Node.js и технологическом стеке Metarhia (gppFXK1YzPA).md)
- Тезисы: обновлённый подход — Metacom RPC поверх WebSocket, прозрачный для клиента