Чеклист нового 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-Keyheader'ом возвращает предыдущий ответ, а не создаёт второй 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 правильно реагирует на плохой ввод».
Связанные разделы¶
../conventions/http-api.md— versioning, response-shape, middleware, timeouts.../how-to/add-http-endpoint.md— пошаговый рецепт с кодом.../conventions/security.md— auth, validation, CORS, rate limit.../conventions/logging.md,../conventions/observability.md— логи, метрики, трейсы.pr-author.md— общий чеклист PR-автора, бежать до этого.