Цели модуля
После модуля студент сможет:
- Применять 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:
- Пишем код хэндлеров
- Генерируем документацию из кода
- Документация может отставать от кода
Contract-first:
- Пишем OpenAPI спецификацию
- Генерируем серверный код или пишем по спецификации
- Код всегда соответствует контракту
Сравнение подходов
| Критерий | Code-first | Contract-first |
|---|---|---|
| Скорость старта | Быстрее | Медленнее |
| Актуальность доков | Может отставать | Всегда актуальны |
| Параллельная работа | Сложнее | Легче |
| Клиентские SDK | Генерация после | Генерация сразу |
| Ревью изменений API | По коду | По спецификации |
Когда что использовать
Code-first когда:
- Маленькая команда
- Быстрое прототипирование
- Внутренний API без внешних клиентов
Contract-first когда:
- Публичный API
- Несколько команд (клиенты и сервер)
- Нужны клиентские SDK
- Строгие требования к документации
Правило
Для публичных API и больших команд используйте contract-first. Для внутренних сервисов допустим code-first с автогенерацией доков.
2.2 OpenAPI-driven development
Процесс contract-first
Шаг 1: Написание спецификации
yamlopenapi: "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 / Prism | Mock сервер по спецификации |
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
- Никогда не удаляйте поля из ответов
- Никогда не изменяйте типы полей
- Новые поля в запросах — optional с дефолтом
- Новые поля в ответах — клиенты должны игнорировать неизвестные
- Используйте 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 полей:
goif _, 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 необходим:
textv1: 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 пагинация: реализация
gotype 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 пагинации
sqlSELECT id, name, owner_id, created_at FROM repos WHERE id > $1 -- after_id ORDER BY id ASC LIMIT $2
Для сортировки по created_at:
sqlSELECT 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 полей:
govar 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
- Per IP: базовая защита от DoS
- Per user: fair use для авторизованных
- Per API key: для внешних интеграций
- Per tenant: для multi-tenant SaaS
Алгоритмы rate limiting
Token bucket:
gotype TokenBucket struct { tokens float64 maxTokens float64 refillRate float64 lastRefill time.Time } func (b *TokenBucket) Allow() bool { // Пополняем токены // Если есть токен — разрешаем }
Sliding window:
gotype SlidingWindow struct { requests []time.Time window time.Duration limit int } func (w *SlidingWindow) Allow() bool { // Удаляем запросы вне окна // Если в окне меньше limit — разрешаем }
Distributed rate limiting
Для нескольких инстансов сервиса нужен распределённый rate limiter.
Redis-based:
gofunc (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 quota | 10000 запросов в день |
| Monthly quota | 100000 запросов в месяц |
| Resource quota | 100 репозиториев на пользователя |
Реализация:
gotype 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
gofunc 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
Что изучаем на примере шаблона
- OpenAPI спецификация в api/openapi.yaml
- Версионирование через URL (/api/v1/)
- Cursor-based пагинация в ListRepos
- Rate limiting middleware
- Error response в формате RFC 7807
Задание: изучите api/openapi.yaml
- Какие эндпоинты описаны?
- Есть ли пагинация?
- Есть ли описание ошибок?
- Совпадает ли спецификация с реализацией?
💱 Компромиссы
- Генерация из OpenAPI vs ручной код:
oapi-codegenдержит код и контракт синхронно, сразу даёт SDK и mock через Prism, но каждая правка API начинается с YAML и регенерации; ручные хэндлеры быстрее итерируются, но спецификация расходится с реализацией. - Cursor по
idvs композитный 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:
gofunc RateLimit(requestsPerMinute int) func(http.Handler) http.Handler
Добавьте заголовки:
- RateLimit-Limit
- RateLimit-Remaining
- RateLimit-Reset
Задание 5: Quotas
Реализуйте quota service:
gofunc (s *QuotaService) CheckAndIncrement(ctx context.Context, userID, resource string) error
Лимиты:
- 100 репозиториев на пользователя
- 1000 запросов в день
Задание 6: Deprecation
Добавьте deprecation headers для устаревшего поля:
goif _, 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 мониторинг
Добавьте метрики:
govar apiRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "api_requests_total", }, []string{"endpoint", "version", "status"}, )
Отслеживайте использование по версиям API.
Quiz "Проверь себя"
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0oapi-codegen -generate types,server -package api api/openapi.yaml > api/server.gen.go✅ Итоги модуля
Студент должен:
- Применять 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/ шаблона.