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)

Связанные темы

Ресурсы


🎓 Источник: Разработка 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, прозрачный для клиента