REST API

Зачем нужно

REST (Representational State Transfer) — архитектурный стиль для построения API. REST определяет правила именования URL, использования HTTP-методов и структуры ответов. Почти все веб-API построены по принципам REST: GitHub API, Twitter API, любой бэкенд для SPA.

Где используется

  • Бэкенд для SPA и мобильных приложений
  • Публичные API (GitHub, Stripe, Twilio)
  • Микросервисная архитектура
  • Интеграция между системами

Принципы REST

  1. Client-Server — клиент и сервер независимы
  2. Stateless — сервер не хранит состояние клиента между запросами
  3. Cacheable — ответы можно кэшировать
  4. Uniform Interface — единообразный интерфейс (URL = ресурс, HTTP-метод = действие)
  5. Layered System — клиент не знает, общается ли он с сервером напрямую или через прокси
  6. Code on Demand (опционально) — сервер может отправить код для выполнения на клиенте

Ресурсы и Endpoints

REST оперирует ресурсами (существительные), а не действиями (глаголы):

✅ Правильно (существительные):
GET    /api/users          — список пользователей
GET    /api/users/42       — конкретный пользователь
POST   /api/users          — создать пользователя
PUT    /api/users/42       — обновить пользователя
DELETE /api/users/42       — удалить пользователя

❌ Неправильно (глаголы):
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser/42
GET /api/getUserById?id=42

Вложенные ресурсы

GET    /api/users/42/posts         — посты пользователя 42
POST   /api/users/42/posts         — создать пост от пользователя 42
GET    /api/users/42/posts/7       — пост #7 пользователя 42

GET    /api/posts/7/comments       — комментарии к посту 7
POST   /api/posts/7/comments       — добавить комментарий к посту 7

Фильтрация, сортировка, пагинация

GET /api/users?role=admin&status=active     — фильтрация
GET /api/users?sort=name&order=asc          — сортировка
GET /api/users?page=2&limit=20              — пагинация
GET /api/users?fields=id,name,email         — выбор полей

GET /api/products?category=electronics&min_price=1000&sort=-price&page=1&limit=10

CRUD-маппинг

Операция HTTP-метод URL Тело запроса Ответ
Create POST /api/users {name, email} 201 + созданный объект
Read (все) GET /api/users 200 + массив
Read (один) GET /api/users/42 200 + объект
Update (полное) PUT /api/users/42 {name, email, age} 200 + обновлённый
Update (частичное) PATCH /api/users/42 {email} 200 + обновлённый
Delete DELETE /api/users/42 204 No Content

Структура ответов

// Успешный ответ — один объект
// GET /api/users/42 → 200
{
  "id": 42,
  "name": "Антон",
  "email": "anton@mail.ru",
  "createdAt": "2024-01-15T10:30:00Z"
}

// Успешный ответ — список с пагинацией
// GET /api/users?page=2&limit=10 → 200
{
  "data": [
    { "id": 41, "name": "Мария" },
    { "id": 42, "name": "Антон" }
  ],
  "pagination": {
    "page": 2,
    "limit": 10,
    "total": 156,
    "totalPages": 16
  }
}

// Ошибка
// POST /api/users → 422
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Ошибка валидации",
    "details": [
      { "field": "email", "message": "Некорректный email" },
      { "field": "name", "message": "Обязательное поле" }
    ]
  }
}

Statelessness (без состояния)

// ❌ Stateful — сервер помнит состояние
// Запрос 1: POST /api/login → сервер запоминает сессию
// Запрос 2: GET /api/profile → сервер ищет сессию в памяти

// ✅ Stateless — каждый запрос самодостаточен
// Каждый запрос содержит токен авторизации
fetch('/api/profile', {
  headers: { 'Authorization': 'Bearer eyJ...' },
});
// Сервер верифицирует токен, не обращаясь к сессиям

HATEOAS

Hypermedia As The Engine Of Application State — ответ содержит ссылки на связанные ресурсы:

// GET /api/users/42 → 200
{
  "id": 42,
  "name": "Антон",
  "email": "anton@mail.ru",
  "_links": {
    "self": { "href": "/api/users/42" },
    "posts": { "href": "/api/users/42/posts" },
    "avatar": { "href": "/api/users/42/avatar" },
    "update": { "href": "/api/users/42", "method": "PATCH" },
    "delete": { "href": "/api/users/42", "method": "DELETE" }
  }
}

Версионирование API

// Вариант 1: В URL (самый распространённый)
GET /api/v1/users
GET /api/v2/users

// Вариант 2: В заголовке
GET /api/users
Accept: application/vnd.myapp.v2+json

// Вариант 3: Query parameter
GET /api/users?version=2

Реализация REST API (Express)

const express = require('express');
const app = express;
app.use(express.json());

let users = [
  { id: 1, name: 'Антон', email: 'anton@mail.ru' },
  { id: 2, name: 'Мария', email: 'maria@mail.ru' },
];
let nextId = 3;

// GET /api/users — список
app.get('/api/users', (req, res) => {
  const { role, sort, page = 1, limit = 10 } = req.query;
  let result = [...users];

  // Фильтрация
  if (role) result = result.filter(u => u.role === role);

  // Сортировка
  if (sort) result.sort((a, b) => a[sort]?.localeCompare(b[sort]));

  // Пагинация
  const start = (page - 1) * limit;
  const paged = result.slice(start, start + Number(limit));

  res.json({
    data: paged,
    pagination: { page: Number(page), limit: Number(limit), total: result.length },
  });
});

// GET /api/users/:id — один
app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === Number(req.params.id));
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});

// POST /api/users — создать
app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(422).json({
      error: { message: 'name и email обязательны' },
    });
  }
  const user = { id: nextId++, name, email };
  users.push(user);
  res.status(201).json(user);
});

// PATCH /api/users/:id — частичное обновление
app.patch('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === Number(req.params.id));
  if (!user) return res.status(404).json({ error: 'Not found' });
  Object.assign(user, req.body);
  res.json(user);
});

// DELETE /api/users/:id — удалить
app.delete('/api/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Not found' });
  users.splice(index, 1);
  res.status(204).send;
});

app.listen(3000);

Частые ошибки

  1. Глаголы в URL/api/getUsers вместо GET /api/users
  2. POST для всего — используют POST даже для получения данных
  3. Неправильные статус-коды — всегда 200, даже при ошибках
  4. Нет пагинации — возвращают все 10000 записей одним ответом
  5. Непоследовательный формат — один endpoint возвращает {data: [...]}, другой — массив напрямую
  6. Множественное число/api/user/42 вместо /api/users/42

Практика

  1. Спроектировать REST API для блога (пользователи, посты, комментарии)
  2. Реализовать CRUD на Express с правильными статус-кодами
  3. Добавить фильтрацию, сортировку и пагинацию
  4. Написать единый формат ошибок
  5. Протестировать API через Postman или curl

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

Ресурсы