JS Tower
Node.jsМодуль 4: HTTP и Express

REST API

Принципы REST и создание API

Цель урока

В этом уроке ты научишься:

  • Понимать принципы REST
  • Проектировать API endpoints
  • Использовать правильные HTTP методы и статус-коды

Принципы REST

REST (Representational State Transfer) — архитектурный стиль для API.

Ключевые принципы

  1. Клиент-сервер — разделение ответственности
  2. Stateless — сервер не хранит состояние клиента
  3. Кэширование — ответы могут кэшироваться
  4. Единообразный интерфейс — стандартные методы и URL
  5. Многоуровневая система — клиент не знает о промежуточных серверах

HTTP методы

МетодОписаниеИдемпотентность
GETПолучение ресурсаДа
POSTСоздание ресурсаНет
PUTПолное обновлениеДа
PATCHЧастичное обновлениеДа
DELETEУдалениеДа

Проектирование URL

Хорошие практики

GET    /api/users           # Список пользователей
GET    /api/users/123       # Один пользователь
POST   /api/users           # Создать пользователя
PUT    /api/users/123       # Обновить пользователя
DELETE /api/users/123       # Удалить пользователя

GET    /api/users/123/posts # Посты пользователя
POST   /api/users/123/posts # Создать пост для пользователя

Плохие практики

GET    /api/getUsers        # Глагол в URL
POST   /api/createUser      # Глагол в URL
GET    /api/user            # Единственное число
DELETE /api/users/delete/123 # Лишний сегмент

HTTP статус-коды

2xx — Успех

КодНазваниеИспользование
200OKGET, PUT, PATCH успешно
201CreatedPOST создал ресурс
204No ContentDELETE успешно

4xx — Ошибка клиента

КодНазваниеИспользование
400Bad RequestНевалидные данные
401UnauthorizedНе авторизован
403ForbiddenНет прав доступа
404Not FoundРесурс не найден
409ConflictКонфликт (дубликат)
422Unprocessable EntityОшибка валидации

5xx — Ошибка сервера

КодНазваниеИспользование
500Internal Server ErrorНеожиданная ошибка
503Service UnavailableСервис недоступен

Полный пример API

const express = require('express');
const router = express.Router();

let users = [];
let nextId = 1;

// GET /api/users
router.get('/', (req, res) => {
  const { page = 1, limit = 10, sort } = req.query;
  
  let result = [...users];
  
  // Сортировка
  if (sort) {
    const [field, order] = sort.split(':');
    result.sort((a, b) => {
      if (order === 'desc') return b[field] > a[field] ? 1 : -1;
      return a[field] > b[field] ? 1 : -1;
    });
  }
  
  // Пагинация
  const start = (page - 1) * limit;
  const paginated = result.slice(start, start + parseInt(limit));
  
  res.json({
    data: paginated,
    meta: {
      total: users.length,
      page: parseInt(page),
      limit: parseInt(limit),
      pages: Math.ceil(users.length / limit)
    }
  });
});

// GET /api/users/:id
router.get('/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json({ data: user });
});

// POST /api/users
router.post('/', (req, res) => {
  const { name, email } = req.body;
  
  // Валидация
  if (!name || !email) {
    return res.status(400).json({ 
      error: 'Name and email are required' 
    });
  }
  
  // Проверка дубликата
  if (users.some(u => u.email === email)) {
    return res.status(409).json({ 
      error: 'Email already exists' 
    });
  }
  
  const user = {
    id: nextId++,
    name,
    email,
    createdAt: new Date()
  };
  
  users.push(user);
  
  res.status(201).json({ data: user });
});

// PUT /api/users/:id
router.put('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  
  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ 
      error: 'Name and email are required' 
    });
  }
  
  users[index] = {
    ...users[index],
    name,
    email,
    updatedAt: new Date()
  };
  
  res.json({ data: users[index] });
});

// PATCH /api/users/:id
router.patch('/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  Object.assign(user, req.body, { updatedAt: new Date() });
  
  res.json({ data: user });
});

// DELETE /api/users/:id
router.delete('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  
  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  users.splice(index, 1);
  
  res.status(204).send();
});

module.exports = router;

Формат ответа

Успешный ответ

{
  "data": {
    "id": 1,
    "name": "Иван",
    "email": "ivan@example.com"
  }
}

Список с пагинацией

{
  "data": [
    { "id": 1, "name": "Иван" },
    { "id": 2, "name": "Мария" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 10,
    "pages": 10
  }
}

Ошибка

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": [
      { "field": "email", "message": "Required" }
    ]
  }
}

Практика

Задание: API для задач

Задача: Создай REST API для управления задачами.

Решение:

const express = require('express');
const router = express.Router();

let tasks = [];
let nextId = 1;

router.get('/', (req, res) => {
  const { status } = req.query;
  let result = tasks;
  
  if (status) {
    result = tasks.filter(t => t.status === status);
  }
  
  res.json({ data: result });
});

router.post('/', (req, res) => {
  const { title } = req.body;
  
  if (!title) {
    return res.status(400).json({ error: 'Title required' });
  }
  
  const task = {
    id: nextId++,
    title,
    status: 'pending',
    createdAt: new Date()
  };
  
  tasks.push(task);
  res.status(201).json({ data: task });
});

router.patch('/:id/complete', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  
  task.status = 'completed';
  task.completedAt = new Date();
  
  res.json({ data: task });
});

module.exports = router;

Проверь себя

  1. Какой HTTP метод для создания ресурса?
  2. Какой статус-код для успешного DELETE?
  3. Почему URL должны быть во множественном числе?