Перейти к содержанию

Чеклист нового HTTP-endpoint'а

Пробегай перед тем, как запросить review на PR, добавляющий новый endpoint. Большинство пунктов автоматизируются через helper'ы и middleware; этот чеклист — контроль, что ты не пропустил ничего. Полные правила — в ../conventions/http-api.md и ../how-to/add-http-endpoint.md.

Контракт

  • Method + path. Совпадают с convention: публичный — /v1/<resource>, сервис-к-сервису — /internal/<resource>. Verb соответствует семантике (GET — чтение, POST — создание, PATCH — частичное обновление, DELETE — удаление).
  • Request DTO — отдельный struct с JSON-тегами snake_case и validator-тегами. Живёт в internal/handler/<resource>_dto.go.
  • Response DTO — отдельный struct (не переиспользуй доменный type). JSON-теги snake_case.
  • Response shape стандартный: {"data": ...} на успех, {"error": {"code", "message", "request_id"}} на ошибку.
  • HTTP-статусы замаплены через mapServiceError: 200/201 на успех, 400 — validation, 401 — auth, 403 — forbidden, 404 — not found, 409 — conflict, 422 — business-rule, 500 — internal. Никакого хардкода http.StatusX в handler'е.
  • Пагинация cursor-based на публичных list-endpoint'ах (?cursor=&limit=). Not offset/page.

Handler

  • Сигнатура стандартная: func (h *XHandler) Method(w http.ResponseWriter, r *http.Request).
  • Декодирование через json.NewDecoder + DisallowUnknownFields.
  • Лимит тела через http.MaxBytesReader (обычно 64 KiB для API, больше — если endpoint принимает файлы).
  • Content-Type проверен на POST/PUT/PATCH: не application/json → 415 Unsupported Media Type.
  • Validation на входе через validator.Struct(&req).
  • Вызов service, не repository напрямую. Handler не лезет в БД через pool.
  • Маппинг sentinel-ошибок через helper, не switch'ом в каждом handler'е.
  • Error-сообщения клиенту generic. Нет err.Error() в ответе клиенту, нет SQL, stack trace, DSN. Детали — в лог.
  • context.Context пробрасывается во все service-вызовы (через r.Context()).

Безопасность

  • Auth middleware подключён в соответствии со scope'ом:
    • публичный /v1/*GatewayAuth (HMAC headers от Gateway),
    • внутренний /internal/*InternalToken (subtle.ConstantTimeCompare),
    • неаутентифицированные (login, register) — явно без GatewayAuth и с rate-limit.
  • Role check (если нужно): RequireRole("admin") после GatewayAuth. Не полагайся только на UI — backend проверяет сам.
  • Rate limit на hot endpoints (auth, upload, search). Fixed window через Redis INCR, fail-open при недоступности Redis.
  • Input validation сильная. Никакой fall-through на service- слой: если handler пропустил невалидный rating, service не должен его чинить — он должен упасть на assert.
  • User-filename не используется как путь на диске/S3. Генерируй <ulid>.<ext> сам (см. ../conventions/security.md §Never trust).
  • Trusted-proxy-заголовки (X-Forwarded-For, X-Real-IP) учитываются только через chimw.RealIP с trusted-сетью, не напрямую из r.Header.
  • SQL через pgx-плейсхолдеры ($1, $2). Никаких fmt.Sprintf с user input.

Тесты

  • Handler-тест через httptest: минимум happy-path + ключевая validation-ошибка + ошибка от service (например, conflict).
  • Service-тест (unit), если в endpoint'е новая бизнес-логика, с fake repository и publisher.
  • Integration-тест, если endpoint пересекает БД/Kafka — testcontainers-Postgres / gochannel.
  • Race detector зелёный: go test -race -count=1 ./....
  • Таблицу с endpoint-scenarios оформи как table-driven test (см. ../conventions/testing.md).

Observability

  • Request logged структурировано через middleware — не пиши slog.Info("incoming request") в handler'е руками.
  • PII в логах замаскирован. Email → m***@example.com, phone → +7***1234. См. ../conventions/logging.md.
  • HTTP-метрика автоматом. http_request_duration_seconds{route, method, status} ставит middleware; route должен быть паттерном (/v1/reviews/{id}), не raw path (/v1/reviews/42).
  • Trace автоматом. otelhttp.NewMiddleware на роутере; handler в бизнес-важных точках может добавить ручной span через tracer.Start(ctx, "service.Method").
  • Бизнес-метрика, если endpoint считает значимый факт (новая регистрация, успешный upload). Через helper metrics.BusinessEvent(ctx, name, attrs...) — одновременно counter и INFO-лог.

Документация

  • OpenAPI обновлён: api/openapi.yaml содержит описание endpoint'а (path, method, request schema, response schema, коды ошибок). Если сервис ещё не держит OpenAPI — создай файл.
  • README сервиса обновлён, если endpoint публичный и поведенчески заметный.
  • Новый доменный термин (например, moderation.verdict) → запись в ../glossary.md.
  • Новое событие, если endpoint публикует в Kafka, → зарегистрировано в ../event-catalog.md (см. ../how-to/add-kafka-event.md).

Конфигурация и deployment

  • Новые env-переменные (если есть) добавлены в .env.example с placeholder'ом и комментарием.
  • internal/config/ парсит и валидирует новые поля (fail-fast на старте для обязательных).
  • Миграция БД применена, если endpoint требует новых таблиц/ колонок. Up + down файлы, advisory lock (см. ../how-to/add-migration.md).
  • Route mounted в internal/handler/router.go с правильным middleware scope: публичные под Group с GatewayAuth, internal под Group с InternalToken.
  • Никаких regression'ов в существующих middleware chain'ах (порядок: RequestID → RealIP → Recoverer → Logger → CORS → Auth → RateLimit).

Общий контроль

  • make lint зелёный.
  • make test зелёный.
  • go mod tidy — diff в go.mod/go.sum только по делу.
  • PR-описание объясняет «зачем endpoint», «что он делает», «как его протестировать».

Часто забываемые проверки

Эти пункты теряются чаще остальных на ревью. Сверься глазами:

  • Response с пустым списком возвращает {"data": []}, не {"data": null}. null ломает фронтенд, который ожидает итерируемое значение.
  • Идемпотентность write-endpoint'ов. Повторный POST /v1/reviews с тем же Idempotency-Key header'ом возвращает предыдущий ответ, а не создаёт второй review. Если сервис пока не поддерживает — хотя бы уникальный constraint на БД-уровне защищает от дублей.
  • Ошибки в формате стандарта. {"error": {"code": "validation_failed", "message": "...", "request_id": "..."}} — никаких custom полей у корня ответа, никаких plain-string ошибок.
  • Таймаут handler'а короче, чем таймаут Gateway'я. Если Gateway держит 30s, handler должен укладываться в 25s и отдавать 504 со своей стороны, а не наружу.
  • r.Body.Close() не нужен — chi/Go сам закрывает; но не переиспользуй r.Body после json.Decode.

Anti-patterns

  • Business logic в handler'е. Если handler содержит цикл, SQL, условия вида «если X, то публикуем событие» — это service-слой, переноси.
  • Передача r/w глубже handler'а. Service-слой оперирует DTO и context.Context, не HTTP-типами.
  • Маппинг ошибок switch'ом в handler'е. Дублируется в каждом endpoint'е, забываешь случай — 500. Централизованный mapServiceError — единственный источник правды.
  • Возврат err.Error() клиенту. Раскрывает внутренности (SQL, пути, структуру БД). Всегда generic-сообщение + request_id для support.
  • Тест только happy-path. Без error-path'ов тест ловит только «handler вообще работает», не «handler правильно реагирует на плохой ввод».

Связанные разделы