REST API: или как не превратить backend в кучу спагетти-кода

Вот вы написали микросервис или бэкенд для мобильного приложения. Все работает, клиент доволен, а через полгода вы понимаете, что:

  • Новые разработчики путаются в эндпоинтах, потому что /users/{id}/profile и /user-profiles/{userId} — это одно и то же, но по-разному названо.
  • Frontend-разработчики ругаются, что у них 404 на /api/v1/users, хотя вы давно перешли на /api/v2/users.
  • В логах валяются запросы с Accept: application/xml, хотя вы давно от этого отказались.
  • При добавлении нового поля в ответ все клиенты падают, потому что вы забыли сделать backward-compatible изменения.

Звучит знакомо? Если да, то REST API — это не просто набор HTTP-методов, а система договорённостей, которая спасает от таких проблем. Но только если её правильно применить.


Проблема: когда REST API превращается в антипаттерн

REST — это не спецификация, а стиль архитектуры, который предполагает:

  1. Ресурсоориентированность: всё, с чем работает клиент, должно быть ресурсом (например, /orders, /users/{id}).
  2. Статус-коды по правилам: 200 — всё ок, 404 — ресурс не найден, 400 — клиентская ошибка, 500 — сервер упал.
  3. Idempotentность: GET /users/1 всегда должен возвращать один и тот же ответ для одного и того же пользователя.
  4. HATEOAS (желательно): клиент должен знать, какие действия доступны для ресурса, глядя на ответ (например, через _links в JSON).

Но на практике многие проекты используют только HTTP-методы и JSON, забывая про остальные принципы. Результат:

  • Эндпоинты растут как грибы после дождя: /create-order, /update-order-status, /get-order-by-id.
  • Клиенты зависят от внутренней структуры данных (например, фронтенд ждёт user.id, а бэкенд возвращает user.userId).
  • Нет документации, потому что "всё очевидно" (спойлер: не очевидно).

Практика: как проектировать REST API, чтобы не сойти с ума

1. Ресурсы, а не действия

Правильно:

  • POST /orders — создать заказ.
  • GET /orders/{id} — получить заказ.
  • PATCH /orders/{id} — обновить статус заказа.

Неправильно:

  • POST /create-order — создаёт заказ.
  • POST /update-order-status — обновляет статус.
  • GET /get-order-by-id — возвращает заказ.

Почему? Потому что ресурс — это сущность, а не действие. Если у вас /orders — это ресурс "заказ", то все операции над ним должны быть выражением через HTTP-методы.

Пример конфигурации Swagger (OpenAPI) для ресурса orders:

paths:
  /orders:
    post:
      summary: Создать новый заказ
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '201':
          description: Заказ создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '400':
          description: Невалидный запрос
  /orders/{id}:
    get:
      summary: Получить заказ по ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Успешно
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '404':
          description: Заказ не найден

2. Статус-коды — это не декорация

Не используйте 200 для ошибок. Не возвращайте 201 при обновлении. Не игнорируйте 4xx и 5xx.

Пример правильного использования статусов:

HTTP-код Использование Пример сценария
200 Успешное выполнение GET/PUT/PATCH GET /users/1 возвращает пользователя
201 Ресурс создан (только для POST) POST /orders создаёт новый заказ
204 Успешное выполнение без тела ответа DELETE /orders/1 удаляет заказ
400 Клиентская ошибка (валидация, параметры) POST /orders с невалидным date
401 Неавторизован Попытка доступа без токена
403 Запрещено Пользователь пытается удалить чужой заказ
404 Ресурс не найден GET /users/999999
500 Серверная ошибка База данных упала

Антипример: Возвращать 200 с полем error в теле ответа. Это нарушает семантику HTTP.


3. Версии API — это не опция, а необходимость

Если вы меняете структуру ответа, обязательно делайте новую версию API. Нет версий? Нет возможности обновляться без перерыва работы клиентов.

Как это сделать на практике:

  1. Добавляйте версию в URL: /api/v1/orders, /api/v2/orders.
  2. Или в заголовок: Accept: application/vnd.company.api.v2+json.
  3. Или в параметр запроса: /orders?version=2.

Пример миграции с v1 на v2:

  • v1: Возвращает user.id и user.name.
  • v2: Возвращает user.userId (длинный UUID) и user.fullName.

Как обработать в бэкенде (псевдокод на Go):

func handleRequest(w http.ResponseWriter, r *http.Request) {
    version := r.URL.Query().Get("version")
    if version == "v2" {
        respondV2(w, r)
    } else {
        respondV1(w, r)
    }
}

func respondV1(w http.ResponseWriter, r *http.Request) {
    user := getUserFromDB(r)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "id":   user.ID,    // int
        "name": user.Name,  // string
    })
}

func respondV2(w http.ResponseWriter, r *http.Request) {
    user := getUserFromDB(r)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "userId": user.UUID,    // string (UUID)
        "fullName": user.FullName,
    })
}

Важно: Не забывайте поддерживать старые версии хотя бы до тех пор, пока все клиенты не перейдут на новую. Автоматизируйте тестирование backward compatibility.


4. Документация — это не отдельный документ, а часть кода

Документация API должна быть:

  1. Автоматически генерируемой (Swagger/OpenAPI, Redoc).
  2. Всегда актуальной (не отстающей от кода).
  3. Доступной для клиентов (хостится вместе с API, например, на /docs).

Пример конфигурации Swagger для Node.js (Express):

const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Orders API',
      version: '1.0.0',
      description: 'API для работы с заказами',
    },
    servers: [
      { url: 'http://localhost:3000/api/v1' },
    ],
  },
  apis: ['./routes/*.js'], // Путь к файлам с роутами
};

const specs = swaggerJsdoc(options);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs));

Что должно быть в документации:

  • Описание каждого эндпоинта.
  • Примеры запросов и ответов.
  • Статус-коды и их значения.
  • Требуемые заголовки (например, Authorization: Bearer <token>).
  • Лимиты на запросы (например, "максимум 100 записей на страницу").

5. Тестирование REST API — это не просто "запустил Postman"

Автоматизированные тесты для API должны проверять:

  1. Статус-коды: 200, 404, 500 и т.д.
  2. Структуру ответа: поля, типы данных.
  3. Idempotentность: повторный GET должен возвращать тот же ответ.
  4. Безопасность: проверка CSRF, CORS, авторизации.
  5. Производительность: время ответа, нагрузка.

Пример теста на Python (pytest + requests):

import requests
import pytest

BASE_URL = "http://localhost:3000/api/v1"

def test_create_order():
    response = requests.post(
        f"{BASE_URL}/orders",
        json={"userId": 1, "items": [{"productId": 1, "quantity": 2}]},
        headers={"Authorization": "Bearer test_token"}
    )
    assert response.status_code == 201
    assert "orderId" in response.json()
    assert response.json()["status"] == "created"

def test_get_nonexistent_order():
    response = requests.get(f"{BASE_URL}/orders/999999")
    assert response.status_code == 404
    assert response.json()["error"] == "Order not found"

Инструменты для тестирования:

  • Postman/Newman (для коллекций запросов).
  • Pact (для контракт-тестирования между сервисами).
  • k6 (для нагрузочного тестирования).

Типичные ошибки и как их избежать

  1. Игнорирование версий API

    • Проблема: Клиенты падают при обновлении структуры ответа.
    • Решение: Всегда поддерживайте backward compatibility или используйте версии в URL.
  2. Неправильное использование HTTP-методов

    • Проблема: POST /users/1 для обновления пользователя (должен быть PATCH или PUT).
    • Решение: Придерживайтесь семантики:
      • GET — чтение.
      • POST — создание.
      • PUT — полное обновление.
      • PATCH — частичное обновление.
      • DELETE — удаление.
  3. Отсутствие валидации на клиентской стороне

    • Проблема: Клиент шлёт невалидные данные, сервер возвращает 500 вместо 400.
    • Решение: Валидируйте данные на сервере и возвращайте 400 с описанием ошибок.
  4. Слишком много эндпоинтов

    • Проблема: /get-users, /create-user, /update-user-status — это антипаттерн.
    • Решение: Группируйте операции по ресурсам: /users, /users/{id}.
  5. Забывчивость с CORS

    • Проблема: Фронтенд не может делать запросы к API из-за неправильных заголовков.
    • Решение: Настройте CORS правильно:
      // Express.js
      app.use(cors({
        origin: ['https://your-frontend.com'],
        methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
        allowedHeaders: ['Content-Type', 'Authorization']
      }));
      
  6. Отсутствие HATEOAS

    • Проблема: Клиент не знает, какие действия доступны для ресурса.
    • Решение: Добавляйте ссылки в ответ:
      {
        "orderId": 123,
        "_links": {
          "self": { "href": "/orders/123" },
          "update": { "href": "/orders/123", "method": "PATCH" },
          "cancel": { "href": "/orders/123/cancel", "method": "POST" }
        }
      }
      
  7. Неэффективные запросы

    • Проблема: Клиент получает лишние данные (например, все поля пользователя, хотя нужны только id и name).
    • Решение: Используйте параметры для фильтрации:
      • /users?fields=id,name,email — возвращает только нужные поля.
      • /users?limit=10&offset=20 — пагинация.

Практический вывод: как не завалить продакшен

  1. Проектируйте API как продукт, а не как временное решение

    • Думайте о клиентах (frontend, мобильные приложения, партнёры).
    • Не меняйте контракты без необходимости.
  2. Автоматизируйте всё

    • Генерация документации (Swagger/OpenAPI).
    • Тестирование (unit, integration, load).
    • Развёртывание (CI/CD с проверкой backward compatibility).
  3. Соблюдайте принципы REST

    • Ресурсы, а не действия.
    • HTTP-методы по назначению.
    • Статус-коды по правилам.
    • Версии для совместимости.
  4. Документируйте изменения

    • Ведите changelog для API.
    • Уведомляйте клиентов о брейкинг-изменениях заранее.
  5. Мониторьте и логируйте

    • Отслеживайте популярные эндпоинты, ошибки, время ответа.
    • Логируйте запросы и ответы для дебага (но не в продакшене без фильтрации!).

Итог: REST API — это не просто "HTTP + JSON". Это система, которая требует дисциплины, но спасает от хаоса в больших проектах. Если вы следуете этим правилам, ваш API будет стабильным, масштабируемым и приятным в сопровождении. А если нет — готовьтесь к ночным вызовам от клиентов и кофе на нервной почве.