MidТеория4 min

API Design

REST best practices, версионирование, пагинация, HATEOAS, API Gateway и BFF паттерн

Принципы хорошего API

Хороший API -- предсказуемый, консистентный и удобный для разработчиков. Как говорил Джошуа Блох (архитектор Java): "Public APIs, like diamonds, are forever."

Принципы API Design:
┌──────────────────────────────────────────────┐
│  1. Консистентность -- одинаковые паттерны   │
│  2. Предсказуемость -- ожидаемое поведение   │
│  3. Простота -- минимальная поверхность API   │
│  4. Обратная совместимость -- не ломать       │
│  5. Самодокументируемость -- понятные имена   │
└──────────────────────────────────────────────┘

REST Best Practices

Именование ресурсов

✅ Правильно (существительные, множественное число):
  GET    /api/v1/orders          -- список заказов
  GET    /api/v1/orders/123      -- конкретный заказ
  POST   /api/v1/orders          -- создать заказ
  PUT    /api/v1/orders/123      -- обновить полностью
  PATCH  /api/v1/orders/123      -- частичное обновление
  DELETE /api/v1/orders/123      -- удалить

✅ Вложенные ресурсы:
  GET    /api/v1/orders/123/items        -- items заказа
  POST   /api/v1/orders/123/items        -- добавить item
  GET    /api/v1/users/456/orders        -- заказы пользователя

❌ Неправильно:
  GET    /api/v1/getOrders               -- глагол в URL
  POST   /api/v1/order/create            -- дублирование метода
  GET    /api/v1/order                   -- единственное число
  POST   /api/v1/orders/123/cancel       -- допустимо как действие*

*Действия (actions) допустимы для операций, которые не укладываются в CRUD:

POST /api/v1/orders/123/cancel
POST /api/v1/orders/123/refund
POST /api/v1/payments/123/capture

HTTP статус коды

Успешные (2xx):
  200 OK          -- GET, PUT, PATCH (с телом)
  201 Created     -- POST (ресурс создан, Location header)
  204 No Content  -- DELETE (успех, без тела)

Ошибки клиента (4xx):
  400 Bad Request     -- невалидные данные
  401 Unauthorized    -- не аутентифицирован
  403 Forbidden       -- нет прав
  404 Not Found       -- ресурс не найден
  409 Conflict        -- конфликт состояния (дубликат)
  422 Unprocessable   -- валидация бизнес-правил
  429 Too Many Req    -- rate limit

Ошибки сервера (5xx):
  500 Internal Error  -- необработанная ошибка
  502 Bad Gateway     -- проблема с upstream
  503 Unavailable     -- сервис перегружен
  504 Gateway Timeout -- upstream не ответил

Стандартный формат ошибок (RFC 7807)

{
  "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient Funds",
  "status": 422,
  "detail": "Account balance is $10.00, but order total is $25.50",
  "instance": "/api/v1/orders/123/payment",
  "errors": [
    {
      "field": "payment_method",
      "message": "Insufficient balance for this payment method"
    }
  ],
  "trace_id": "abc-123-def-456"
}

Пагинация

Offset-based (простая, но проблемная)

GET /api/v1/orders?page=5&per_page=20

Ответ:
{
  "data": [...],
  "meta": {
    "page": 5,
    "per_page": 20,
    "total": 1847,
    "total_pages": 93
  }
}

Проблема: OFFSET 100000 LIMIT 20 -- медленный запрос
Проблема: Если между запросами добавили запись -- дубликат

Cursor-based (правильный подход)

GET /api/v1/orders?cursor=eyJpZCI6MTIzfQ&limit=20

Ответ:
{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6MTQzfQ",
    "has_more": true
  }
}

Cursor = Base64 encoded {"id": 123}
SQL: WHERE id > 123 ORDER BY id ASC LIMIT 20

Преимущества:
  + Стабильная производительность (O(1) vs O(n))
  + Нет пропусков/дубликатов при параллельных записях
  + Подходит для infinite scroll

Недостатки:
  - Нельзя перейти на страницу N
  - Нет total count (дорогой запрос)

Keyset Pagination для сложных сортировок

GET /api/v1/orders?sort=created_at&after_date=2026-01-15&after_id=456&limit=20

SQL: WHERE (created_at, id) > ('2026-01-15', 456)
     ORDER BY created_at ASC, id ASC
     LIMIT 20

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

Стратегии версионирования

1. URL Path (самый распространённый):
   GET /api/v1/orders
   GET /api/v2/orders

2. Header:
   GET /api/orders
   Accept: application/vnd.myapi.v2+json

3. Query Parameter:
   GET /api/orders?version=2

4. Content Negotiation:
   GET /api/orders
   Accept: application/json; version=2

Когда нужна новая версия

Breaking Changes (нужна новая версия):
  ❌ Удаление поля
  ❌ Переименование поля
  ❌ Изменение типа поля
  ❌ Изменение семантики
  ❌ Обязательный новый параметр

Non-Breaking Changes (БЕЗ новой версии):
  ✅ Добавление необязательного поля
  ✅ Добавление нового endpoint
  ✅ Добавление необязательного параметра
  ✅ Расширение enum (с осторожностью)

Стратегия совместимости Stripe

Stripe -- эталон API design. Их подход:

  • URL версия (/v1/) меняется крайне редко
  • Каждый аккаунт привязан к "API version" (дата, например 2025-12-15)
  • Новые версии -- автоматическая миграция через transform layer
  • Старые версии поддерживаются годами
┌──────────┐   /v1/charges   ┌──────────────────┐
│  Client  │────────────────>│   API Gateway    │
│  v2024   │  Stripe-Version │                  │
│          │  :2024-06-20    │  Transform Layer │
└──────────┘                 │  2024 -> 2025    │
                             │  2025 -> current │
                             └────────┬─────────┘
                                      │
                                      v
                             ┌────────────────┐
                             │  Current Code  │
                             │  (latest ver.) │
                             └────────────────┘

HATEOAS

Hypermedia as the Engine of Application State -- клиент навигирует по API через ссылки в ответе.

{
  "id": "order-123",
  "status": "pending_payment",
  "total": 2550,
  "_links": {
    "self": { "href": "/api/v1/orders/order-123" },
    "pay": { "href": "/api/v1/orders/order-123/pay", "method": "POST" },
    "cancel": { "href": "/api/v1/orders/order-123/cancel", "method": "POST" },
    "items": { "href": "/api/v1/orders/order-123/items" }
  }
}

Доступные действия зависят от состояния. Если заказ оплачен -- ссылки pay не будет, но появится refund.

На практике: HATEOAS полностью реализуют редко. Чаще используют частичный подход -- ссылки на связанные ресурсы для навигации.

API Gateway

Зачем нужен API Gateway

Без API Gateway:
┌──────────┐──── /users ────> User Service (port 8001)
│  Client  │──── /orders ───> Order Service (port 8002)
│          │──── /payments ─> Payment Service (port 8003)
└──────────┘
  Клиент знает адреса всех сервисов -- плохо

С API Gateway:
┌──────────┐     ┌──────────────┐     ┌──────────┐
│  Client  │────>│ API Gateway  │────>│ Services │
│          │     │              │     │          │
│  Один    │     │ Routing      │     │ User     │
│  адрес   │     │ Auth         │     │ Order    │
│          │     │ Rate Limit   │     │ Payment  │
│          │     │ Caching      │     │          │
│          │     │ Logging      │     │          │
│          │     │ Transform    │     │          │
└──────────┘     └──────────────┘     └──────────┘

Функции API Gateway

Функция Описание
Routing Направление запросов к нужному сервису
Authentication Проверка JWT/OAuth токенов
Rate Limiting Ограничение запросов per client
Caching Кеширование GET запросов
Request/Response Transform Преобразование форматов
Load Balancing Распределение нагрузки
Circuit Breaking Защита от каскадных сбоев
Logging/Tracing Централизованное логирование

Популярные решения

┌─────────────────────────────────────────────┐
│  Kong           -- Lua/Nginx, plugins       │
│  AWS API GW     -- Managed, Lambda интегр.  │
│  Envoy          -- C++, sidecar proxy       │
│  Traefik        -- Go, авто-конфигурация    │
│  APISIX         -- Lua/Nginx, высокая произв│
│  KrakenD        -- Go, stateless, быстрый   │
└─────────────────────────────────────────────┘

BFF Pattern (Backend for Frontend)

Один API Gateway -- не всегда хорошо. Разные клиенты (web, mobile, IoT) имеют разные потребности.

Без BFF:
┌──────┐  ┌──────┐  ┌──────┐
│ Web  │  │Mobile│  │  TV  │
└──┬───┘  └──┬───┘  └──┬───┘
   │         │         │
   └─────────┼─────────┘
             │
    ┌────────┴────────┐
    │  Generic API    │  -- пытается угодить всем
    │  Gateway        │  -- толстые ответы для mobile
    │                 │  -- тонкие ответы для web
    └─────────────────┘

С BFF:
┌──────┐     ┌──────┐     ┌──────┐
│ Web  │     │Mobile│     │  TV  │
└──┬───┘     └──┬───┘     └──┬───┘
   │            │            │
   v            v            v
┌──────┐  ┌────────┐  ┌─────────┐
│ Web  │  │ Mobile │  │  TV     │
│ BFF  │  │  BFF   │  │  BFF   │
└──┬───┘  └───┬────┘  └───┬────┘
   │          │            │
   └──────────┼────────────┘
              │
     ┌────────┴────────┐
     │   Microservices │
     └─────────────────┘

Когда использовать BFF

  • Mobile нужны минимальные ответы (трафик, батарея)
  • Web нужна агрегация данных для SPA
  • TV/IoT нужны совсем другие форматы
  • Разные команды разрабатывают разные клиенты

Netflix пример

Netflix имеет отдельные BFF для каждой платформы:

  • iOS BFF -- оптимизирован для iPhone/iPad
  • Android BFF -- учитывает разнообразие устройств
  • TV BFF -- упрощённый UI, большие изображения
  • Web BFF -- полнофункциональный

Каждый BFF вызывает одни и те же backend-сервисы, но формирует ответ под свой клиент.

API Documentation

OpenAPI (Swagger)

# openapi.yaml
openapi: 3.1.0
info:
  title: Order Service API
  version: 1.0.0

paths:
  /api/v1/orders:
    get:
      summary: List orders
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Orders list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'
    post:
      summary: Create order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
          headers:
            Location:
              schema:
                type: string

Правила документации API

  1. Генерировать из кода -- OpenAPI spec должна генерироваться автоматически
  2. Примеры -- каждый endpoint имеет реальный пример запроса/ответа
  3. Описание ошибок -- каждый код ошибки задокументирован
  4. Changelog -- история изменений API

Проверь себя

Зачем нужен BFF (Backend for Frontend) паттерн?

Какое изменение API НЕ является breaking change?

Что описывает формат RFC 7807 (Problem Details)?

Что делает API Gateway?

Почему cursor-based пагинация предпочтительнее offset-based?