Цели модуля

После модуля студент сможет:

  • Применять contract-first подход к разработке API
  • Генерировать серверный код из OpenAPI спецификации
  • Обеспечивать backward compatibility при эволюции API
  • Реализовывать cursor-based пагинацию для больших датасетов
  • Проектировать rate limiting и quotas на масштабе
  • Управлять жизненным циклом API (versioning, deprecation)
  • Понимать паттерны API Gateway и Backend-for-Frontend

2.1 Contract-first vs Code-first

Два подхода к разработке API

Code-first:

  1. Пишем код хэндлеров
  2. Генерируем документацию из кода
  3. Документация может отставать от кода

Contract-first:

  1. Пишем OpenAPI спецификацию
  2. Генерируем серверный код или пишем по спецификации
  3. Код всегда соответствует контракту

Сравнение подходов

КритерийCode-firstContract-first
Скорость стартаБыстрееМедленнее
Актуальность доковМожет отставатьВсегда актуальны
Параллельная работаСложнееЛегче
Клиентские SDKГенерация послеГенерация сразу
Ревью изменений APIПо кодуПо спецификации

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

Code-first когда:

  • Маленькая команда
  • Быстрое прототипирование
  • Внутренний API без внешних клиентов

Contract-first когда:

  • Публичный API
  • Несколько команд (клиенты и сервер)
  • Нужны клиентские SDK
  • Строгие требования к документации

Правило

Для публичных API и больших команд используйте contract-first. Для внутренних сервисов допустим code-first с автогенерацией доков.


2.2 OpenAPI-driven development

Процесс contract-first

Шаг 1: Написание спецификации

yaml
openapi: "3.0.3"
info:
  title: Repo Service API
  version: "1.0.0"
paths:
  /repos:
    get:
      operationId: listRepos
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: List of repositories
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepoListResponse"

Шаг 2: Генерация серверного кода

bash
# oapi-codegen - генератор Go кода из OpenAPI
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest

oapi-codegen -generate types,server -package api api/openapi.yaml > api/server.gen.go

Шаг 3: Реализация интерфейсов

go
// oapi-codegen генерирует интерфейс
type ServerInterface interface {
    ListRepos(w http.ResponseWriter, r *http.Request, params ListReposParams)
    CreateRepo(w http.ResponseWriter, r *http.Request)
    GetRepo(w http.ResponseWriter, r *http.Request, id string)
}

// Реализуем интерфейс
type Handler struct {
    usecase *usecase.ListReposUseCase
}

func (h *Handler) ListRepos(w http.ResponseWriter, r *http.Request, params ListReposParams) {
    // Реализация
}

Шаг 4: Валидация запросов по спецификации

go
// Middleware для валидации по OpenAPI
import "github.com/getkin/kin-openapi/openapi3filter"

func OpenAPIValidation(spec *openapi3.T) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            route, pathParams, err := findRoute(spec, r)
            if err != nil {
                writeProblem(w, 404, "Not Found", "route not found")
                return
            }

            if err := openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
                Request:    r,
                PathParams: pathParams,
                Route:      route,
            }); err != nil {
                writeProblem(w, 400, "Bad Request", err.Error())
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Инструменты для OpenAPI

ИнструментНазначение
oapi-codegenГенерация Go кода из OpenAPI
kin-openapiВалидация запросов по спецификации
swagger-cliВалидация и бандлинг спецификации
redoclyДокументация и linting
Mockoon / PrismMock сервер по спецификации

Mock сервер для параллельной разработки

Пока серверная команда реализует API, клиентская работает с mock:

bash
# Prism - mock сервер по OpenAPI
prism mock api/openapi.yaml

Клиенты могут тестировать интеграцию до готовности сервера.


2.3 Backward compatibility

Что такое breaking change

Breaking change ломает существующих клиентов:

Breaking changes:

  • Удаление поля из ответа
  • Изменение типа поля (string -> int)
  • Изменение обязательности поля (optional -> required)
  • Удаление эндпоинта
  • Изменение кодов ответа
  • Переименование поля

Non-breaking changes:

  • Добавление нового поля в ответ
  • Добавление нового эндпоинта
  • Добавление optional поля в запрос
  • Добавление нового кода ответа
  • Добавление нового значения в enum (осторожно!)

Правила backward compatibility

  1. Никогда не удаляйте поля из ответов
  2. Никогда не изменяйте типы полей
  3. Новые поля в запросах — optional с дефолтом
  4. Новые поля в ответах — клиенты должны игнорировать неизвестные
  5. Используйте deprecation process для удаления

Deprecation process

Шаг 1: Анонс

text
# В документации и заголовках
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"

Шаг 2: Мониторинг использования

Логируйте использование deprecated полей:

go
if _, exists := req.Body["legacy_field"]; exists {
    metrics.DeprecatedFieldUsage.Inc()
    logger.Warn("deprecated field used", "field", "legacy_field")
}

Шаг 3: Период поддержки

Минимум 6 месяцев для внешних API. Уведомляйте клиентов.

Шаг 4: Удаление

Только после подтверждения что клиенты мигрировали.

Версионирование для breaking changes

Если breaking change необходим:

text
v1: GET /api/v1/repos/{id}
v2: GET /api/v2/repos/{id}

Поддерживайте обе версии в течение deprecation периода.

Пример: эволюция поля

json
// v1: поле name
{
    "id": "repo-123",
    "name": "my-project"
}
json
// v2: добавлено display_name, name осталось
{
    "id": "repo-123",
    "name": "my-project",
    "display_name": "My Project"
}
json
// v3 (через год): name deprecated, но всё ещё возвращается
{
    "id": "repo-123",
    "name": "my-project",
    "display_name": "My Project",
    "_deprecated": ["name"]
}

2.4 Pagination, filtering, sorting (продвинуто)

Типы пагинации:

Offset-based: GET /api/v1/repos?limit=20&offset=40

  • Плюсы: простота, произвольный доступ к странице.
  • Минусы: медленно на больших offset, дубли при изменениях.

Cursor-based: GET /api/v1/repos?limit=20&cursor=eyJpZCI6MTAwfQ==

  • Плюсы: стабильность, быстро на больших датасетах.
  • Минусы: нет произвольного доступа, сложнее реализовать.

Keyset pagination: GET /api/v1/repos?limit=20&after_id=repo-100

  • Плюсы: быстро, стабильно.
  • Минусы: только последовательный доступ.

Рекомендация

  • Для публичных API с большими датасетами: cursor-based.
  • Для внутренних API с малыми данными: offset-based.

Cursor-based пагинация: реализация

go
type RepoFilter struct {
    Cursor string
    Limit  int
}

type RepoListResponse struct {
    Items      []*RepoResponse `json:"items"`
    NextCursor string          `json:"next_cursor,omitempty"`
    HasMore    bool            `json:"has_more"`
}

func (h *Handler) ListRepos(w http.ResponseWriter, r *http.Request) {
    cursor := r.URL.Query().Get("cursor")
    limit := parseIntParam(r, "limit", 20)

    // Декодируем cursor
    var afterID string
    if cursor != "" {
        decoded, err := base64.URLEncoding.DecodeString(cursor)
        if err != nil {
            writeProblem(w, 400, "Bad Request", "invalid cursor")
            return
        }
        afterID = string(decoded)
    }

    // Запрашиваем limit + 1 для определения has_more
    repos, err := h.usecase.Execute(r.Context(), RepoFilter{
        AfterID: afterID,
        Limit:   limit + 1,
    })
    if err != nil {
        h.handleError(w, err)
        return
    }

    hasMore := len(repos) > limit
    if hasMore {
        repos = repos[:limit]
    }

    var nextCursor string
    if hasMore && len(repos) > 0 {
        lastID := repos[len(repos)-1].ID
        nextCursor = base64.URLEncoding.EncodeToString([]byte(lastID))
    }

    h.writeJSON(w, http.StatusOK, RepoListResponse{
        Items:      mapRepos(repos),
        NextCursor: nextCursor,
        HasMore:    hasMore,
    })
}

SQL для cursor-based пагинации

sql
SELECT id, name, owner_id, created_at
FROM repos
WHERE id > $1  -- after_id
ORDER BY id ASC
LIMIT $2

Для сортировки по created_at:

sql
SELECT id, name, owner_id, created_at
FROM repos
WHERE (created_at, id) < ($1, $2)  -- keyset pagination
ORDER BY created_at DESC, id DESC
LIMIT $3

Filtering

Стандартный подход: query параметры для простых фильтров.

GET /api/v1/repos?owner_id=user-123&is_private=true&created_after=2026-01-01`

Для сложных фильтров: отдельный эндпоинт или POST с body.

POST /api/v1/repos/search
{
    "filters": {
        "owner_id": "user-123",
        "created_after": "2026-01-01",
        "tags": ["go", "backend"]
    },
    "limit": 20,
    "cursor": "..."
}

Sorting

GET /api/v1/repos?sort=created_at&order=desc

Валидация sort полей:

go
var allowedSortFields = map[string]bool{
    "created_at": true,
    "name":       true,
    "updated_at": true,
}

func validateSortField(field string) error {
    if !allowedSortFields[field] {
        return fmt.Errorf("invalid sort field: %s", field)
    }
    return nil
}

2.5 Rate limiting и quotas на масштабе

Уровни rate limiting

  1. Per IP: базовая защита от DoS
  2. Per user: fair use для авторизованных
  3. Per API key: для внешних интеграций
  4. Per tenant: для multi-tenant SaaS

Алгоритмы rate limiting

Token bucket:

go
type TokenBucket struct {
    tokens     float64
    maxTokens  float64
    refillRate float64
    lastRefill time.Time
}

func (b *TokenBucket) Allow() bool {
    // Пополняем токены
    // Если есть токен — разрешаем
}

Sliding window:

go
type SlidingWindow struct {
    requests []time.Time
    window   time.Duration
    limit    int
}

func (w *SlidingWindow) Allow() bool {
    // Удаляем запросы вне окна
    // Если в окне меньше limit — разрешаем
}

Distributed rate limiting

Для нескольких инстансов сервиса нужен распределённый rate limiter.

Redis-based:

go
func (l *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
    // INCR key
    // EXPIRE key window
    // Проверяем count <= limit
}

// Lua скрипт для атомарности
var rateLimitScript = redis.NewScript(`
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local window = tonumber(ARGV[2])

    local current = redis.call('INCR', key)
    if current == 1 then
        redis.call('EXPIRE', key, window)
    end

    return current <= limit
`)

Quotas

Quotas — долгосрочные лимиты (в отличие от rate limiting):

ТипПример
Daily quota10000 запросов в день
Monthly quota100000 запросов в месяц
Resource quota100 репозиториев на пользователя

Реализация:

go
type QuotaService struct {
    redis *redis.Client
}

func (s *QuotaService) CheckAndIncrement(ctx context.Context, userID, resource string) error {
    key := fmt.Sprintf("quota:%s:%s:%s", userID, resource, time.Now().Format("2006-01"))

    count, err := s.redis.Incr(ctx, key).Result()
    if err != nil {
        return fmt.Errorf("check quota: %w", err)
    }

    if count == 1 {
        s.redis.Expire(ctx, key, 32*24*time.Hour)
    }

    limit := s.getLimit(userID, resource)
    if count > limit {
        return &QuotaExceededError{Resource: resource, Limit: limit}
    }

    return nil
}

Заголовки rate limiting

Стандартные заголовки (RFC 6585, draft-ietf-httpapi-ratelimit-headers):

RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1640995200

# При превышении
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0

Middleware

go
func RateLimit(limiter RateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            key := extractKey(r)  // IP, user ID, API key

            allowed, remaining, resetAt := limiter.Check(r.Context(), key)

            w.Header().Set("RateLimit-Limit", strconv.Itoa(limiter.Limit()))
            w.Header().Set("RateLimit-Remaining", strconv.Itoa(remaining))
            w.Header().Set("RateLimit-Reset", strconv.FormatInt(resetAt.Unix(), 10))

            if !allowed {
                w.Header().Set("Retry-After", "60")
                writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "rate limit exceeded")
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

2.6 API lifecycle management

Стадии жизненного цикла API

Design -> Development -> Testing -> Release -> Maintenance -> Deprecation -> Retirement

Design

  • Contract-first: пишем OpenAPI спецификацию
  • Ревью спецификации командой
  • Mock сервер для клиентов

Development

  • Генерация серверного кода
  • Реализация хэндлеров
  • Валидация по спецификации

Testing

  • Контрактные тесты (спецификация vs реализация)
  • Интеграционные тесты
  • Нагрузочное тестирование

Release

  • Версионирование
  • Changelog
  • Миграционные гайды для клиентов

Maintenance

  • Мониторинг использования
  • Сбор фидбека
  • Исправление багов

Deprecation

  • Анонс с датой sunset
  • Мониторинг использования deprecated фич
  • Миграционные гайды

Retirement

  • Удаление после deprecation периода
  • Архивирование документации

Changelog

Ведите changelog для каждой версии:

## [2.1.0] - 2026-01-15

### Added
- GET /api/v2/repos/{id}/stats - статистика репозитория
- Поле display_name в ответе Repo

### Changed
- Увеличен лимит пагинации с 100 до 500

### Deprecated
- Поле name будет удалено в v3.0. Используйте display_name

### Fixed
- Исправлена сортировка по created_at

API мониторинг

Отслеживайте:

  • Использование эндпоинтов
  • Использование deprecated полей
  • Error rate по версиям
  • Latency по эндпоинтам
  • Клиентов по версиям API

2.7 API Gateway и BFF

API Gateway

Единая точка входа для всех API. Отвечает за:

  • Аутентификацию и авторизацию
  • Rate limiting
  • Маршрутизацию запросов
  • Трансформацию запросов/ответов
  • Логирование и мониторинг
  • Caching

Популярные API Gateway:

  • Kong
  • Envoy
  • AWS API Gateway
  • Yandex API Gateway
  • Traefik

Когда нужен API Gateway

  • Много микросервисов
  • Единая точка аутентификации
  • Разные протоколы (HTTP, gRPC, WebSocket)
  • A/B тестирование
  • Canary routing

Backend-for-Frontend (BFF)

Отдельный backend для каждого типа клиента:

Web BFF      -> оптимизирован для web-приложения
Mobile BFF   -> оптимизирован для мобильного приложения
Partner BFF  -> для внешних интеграций

Каждый BFF:

  • Агрегирует данные из нескольких сервисов
  • Форматирует ответы под нужды клиента
  • Реализует клиент-специфичную логику

Пример BFF

go
// Web BFF: агрегирует данные для главной страницы
func (h *WebBFFHandler) GetDashboard(w http.ResponseWriter, r *http.Request) {
    userID := GetUserID(r.Context())

    // Параллельные запросы к сервисам
    var wg sync.WaitGroup
    var repos []*Repo
    var notifications []*Notification
    var stats *Stats

    wg.Add(3)
    go func() {
        defer wg.Done()
        repos, _ = repoService.List(r.Context(), userID)
    }()
    go func() {
        defer wg.Done()
        notifications, _ = notifService.List(r.Context(), userID)
    }()
    go func() {
        defer wg.Done()
        stats, _ = statsService.Get(r.Context(), userID)
    }()
    wg.Wait()

    // Агрегированный ответ
    h.writeJSON(w, http.StatusOK, DashboardResponse{
        Repos:         repos,
        Notifications: notifications,
        Stats:         stats,
    })
}

Когда нужен BFF

  • Разные требования у web и mobile
  • Нужна агрегация из многих сервисов
  • Клиент-специфичная логика
  • Оптимизация payload

Привязка к шаблону проекта

Где API Design в шаблоне

api/
└── openapi.yaml                  # OpenAPI спецификация

internal/handlers/http/api/v1/
├── handler.go                    # HTTP хэндлеры
├── dto.go                        # DTO с контрактами
├── mapper.go                     # Мапперы
└── routes.go                     # Регистрация роутов

internal/handlers/http/middleware/
└── rate_limit.go                 # Rate limiting

Что изучаем на примере шаблона

  1. OpenAPI спецификация в api/openapi.yaml
  2. Версионирование через URL (/api/v1/)
  3. Cursor-based пагинация в ListRepos
  4. Rate limiting middleware
  5. Error response в формате RFC 7807

Задание: изучите api/openapi.yaml

  • Какие эндпоинты описаны?
  • Есть ли пагинация?
  • Есть ли описание ошибок?
  • Совпадает ли спецификация с реализацией?

💱 Компромиссы

  • Генерация из OpenAPI vs ручной код: oapi-codegen держит код и контракт синхронно, сразу даёт SDK и mock через Prism, но каждая правка API начинается с YAML и регенерации; ручные хэндлеры быстрее итерируются, но спецификация расходится с реализацией.
  • Cursor по id vs композитный keyset (created_at, id): простой cursor дешевле кодить, но листание идёт только в порядке id; композитный keyset даёт сортировку по дате, но требует tuple-сравнения в SQL и уникального tie-breaker.
  • Redis rate limiter vs локальный счётчик: лимиты в Redis с Lua-скриптом едины для всех инстансов, но это зависимость и задержка на каждый запрос; локальный in-memory быстр, но у каждого инстанса свой счётчик и суммарный лимит уплывает.
  • Долгий deprecation vs скорость эволюции: период поддержки v1 и v2 минимум 6 месяцев не ломает клиентов, но команде дольше жить с двумя версиями и мониторить использование deprecated полей.
  • BFF vs единый API: отдельный backend для web и mobile агрегирует данные и режет payload под клиента, но это ещё один сервис в обслуживании; единый API проще, но клиенты сами собирают экран из многих запросов и тащат лишнее.

Практическое задание

Задание 1: Contract-first

Напишите OpenAPI спецификацию для нового эндпоинта:

GET /api/v1/repos/{id}/stats

Ответ:

json
{
    "repo_id": "repo-123",
    "total_commits": 150,
    "total_branches": 5,
    "last_commit_at": "2026-01-15T10:00:00Z"
}

Сгенерируйте серверный код через oapi-codegen.

Задание 2: Cursor-based пагинация

Реализуйте cursor-based пагинацию для ListRepos:

GET /api/v1/repos?limit=20&cursor=eyJpZCI6MTAwfQ==

Ответ:

json
{
    "items": [...],
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "has_more": true
}

Задание 3: Backward compatibility

Добавьте новое поле display_name в RepoResponse:

json
{
    "id": "repo-123",
    "name": "my-project",
    "display_name": "My Project"
}

Убедитесь что существующие клиенты не сломаются.

Задание 4: Rate limiting

Реализуйте rate limiter middleware:

go
func RateLimit(requestsPerMinute int) func(http.Handler) http.Handler

Добавьте заголовки:

  • RateLimit-Limit
  • RateLimit-Remaining
  • RateLimit-Reset

Задание 5: Quotas

Реализуйте quota service:

go
func (s *QuotaService) CheckAndIncrement(ctx context.Context, userID, resource string) error

Лимиты:

  • 100 репозиториев на пользователя
  • 1000 запросов в день

Задание 6: Deprecation

Добавьте deprecation headers для устаревшего поля:

go
if _, exists := req.Body["legacy_field"]; exists {
    w.Header().Set("Deprecation", "true")
    w.Header().Set("Sunset", "Sat, 01 Jan 2027 00:00:00 GMT")
}

Задание 7: API мониторинг

Добавьте метрики:

go
var apiRequestsTotal = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "api_requests_total",
    },
    []string{"endpoint", "version", "status"},
)

Отслеживайте использование по версиям API.


Quiz "Проверь себя"

1. Что такое BFF (Backend-for-Frontend)?
2. Как реализовать rate limiting для нескольких инстансов сервиса?
3. Что означает этот ответ сервера?
text
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0
4. Как правильно удалять поле из публичного API?
5. Какие обязанности у API Gateway? — выберите все подходящие
6. Какие изменения в API являются breaking? — выберите все подходящие
7. Что такое contract-first development?
8. Какая команда генерирует серверный код из OpenAPI-спецификации?
bash
oapi-codegen -generate types,server -package api api/openapi.yaml > api/server.gen.go
9. Чем cursor-based пагинация лучше offset-based на больших датасетах?

✅ Итоги модуля

Студент должен:

  • Применять contract-first подход
  • Генерировать код из OpenAPI
  • Обеспечивать backward compatibility
  • Реализовывать cursor-based пагинацию
  • Проектировать rate limiting на масштабе
  • Управлять жизненным циклом API
  • Понимать API Gateway и BFF паттерны
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 3

Следующий модуль: Модуль 3. Distributed Systems Patterns - eventual consistency, retries and backoff, idempotency, outbox pattern, saga orchestration, DLQ and compensation flows. Привязка к internal/usecase/ и internal/handlers/jobs/ шаблона.