Опирается на правила: R-OAS-1..4, R-URL-X1..X4, R-MTH-X1, R-NEST-X1..X3, R-VER-X1..X4, R-QRY-X1..X5, R-RSP-X1..X4, R-HDR-X1, R-ERR-X1..X4, R-RATE-X1, R-DEP-X1, R-ALIAS-X1..X2, R-ACT-X1..X2, R-PRIN-X1, R-LOC-X1раздел OpenAPI-метаданные и антипаттерны.

Важно знать

  • В Go нет встроенной генерации OpenAPI — спека ведётся через swaggo/swag-аннотации или вручную рядом с кодом; структуры Go — источник контракта.
  • operationId — camelCase, формат действие + ресурс (createOrder, confirmOrder); без него SDK-генератор создаёт имена вида postOrdersIdConfirm.
  • tags — один тег на ресурс, множественное число с заглавной (Orders, Products); action-эндпоинты получают тег родительского ресурса.
  • Параметры пути в chi используют {id} (контекст — из имени сегмента); в OpenAPI-спеке параметры именуются уникально (orderId, itemId) — Swagger UI/Redoc не работают с одинаковыми именами.
  • omitempty на всех необязательных полях response-структур закрывает R-RSP-X1 (нет null в 2xx); *T-указатели в response тянут null — только для PATCH-запросов.
  • Валидация → 400 Bad Request через go-playground/validator + violations; 422 — за пределами допустимого списка кодов (R-ERR-X3).
  • Единый httperr.Write через весь сервис закрывает R-PRIN-2 (единообразие); разные структуры ошибок в разных хендлерах — антипаттерн.

operationId

R-OAS-1: каждая операция получает уникальный operationId в camelCase. Паттерн: действие + ресурс.

// createOrder godoc
// @Summary      Создать заказ
// @Tags         Orders
// @Accept       json
// @Produce      json
// @Param        body body CreateOrderRequest true "Параметры заказа"
// @Success      201 {object} OrderResponse
// @Failure      400 {object} ValidationProblem
// @Failure      409 {object} ProblemDetails
// @Failure      500 {object} ProblemDetails
// @Router       /api/v1/orders [post]
// @operationId  createOrder
func createOrder(w http.ResponseWriter, r *http.Request) { ... }

// confirmOrder godoc
// @Summary      Подтвердить заказ
// @Tags         Orders
// @Param        orderId path string true "ID заказа"
// @Success      200 {object} OrderResponse
// @Failure      404 {object} ProblemDetails
// @Failure      409 {object} ProblemDetails
// @Router       /api/v1/orders/{orderId}/confirm [post]
// @operationId  confirmOrder
func confirmOrder(w http.ResponseWriter, r *http.Request) { ... }

Конвенция именования:

ОперацияoperationId
GET /ordersgetOrders
GET /orders/{id}getOrder
POST /orderscreateOrder
PUT /orders/{id}updateOrder
PATCH /orders/{id}patchOrder
DELETE /orders/{id}deleteOrder
POST /orders/{id}/confirmconfirmOrder
POST /orders/searchsearchOrders
GET /products/{id}/price-historygetProductPriceHistory
POST /customers/{id}/verifyverifyCustomer

SDK-генераторы (openapi-generator) используют operationId как имя метода. Без него генерируется что-то вроде postApiV1OrdersOrderIdConfirm — нечитаемо и нестабильно при изменении маршрута.

tags

R-OAS-2: один тег на ресурс. Action-эндпоинты — тег родительского ресурса.

// @Tags Orders       ← createOrder, getOrder, confirmOrder, cancelOrder
// @Tags Products     ← getProduct, createProduct, listProductReviews
// @Tags Customers    ← getCustomer, verifyCustomer

В chi-роутере тег соответствует блоку r.Route:

r.Route("/api/v1", func(r chi.Router) {
    r.Route("/orders", func(r chi.Router) {          // тег Orders
        r.Get("/", listOrders)                        // getOrders
        r.Post("/", createOrder)                      // createOrder
        r.Route("/{id}", func(r chi.Router) {
            r.Get("/", getOrder)                      // getOrder
            r.Post("/confirm", confirmOrder)          // confirmOrder → тег Orders
            r.Post("/cancel", cancelOrder)            // cancelOrder → тег Orders
        })
    })
    r.Route("/products", func(r chi.Router) {        // тег Products
        r.Get("/", listProducts)
        r.Get("/{id}", getProduct)
    })
})

Swagger UI группирует эндпоинты по тегам. Без тегов или с плоским списком — 50+ операций без навигации.

Параметры пути: {id} в chi vs уникальные имена в OpenAPI

R-OAS-3 и R-NEST-4 задают двойной стандарт:

// chi-маршрут — {id} в дизайне URL, контекст даёт сегмент ресурса
r.Get("/orders/{id}/items/{id}", getOrderItem)  // ← так в дизайне

// chi-обработчик — читаем chi.URLParam по имени
orderID := chi.URLParam(r, "id")   // первый {id} — orderId по контексту
itemID  := chi.URLParam(r, "id")   // второй {id} — itemId по контексту

В chi параметры с одинаковым именем в одном пути — ошибка маршрутизации. Реальный роутер:

r.Get("/orders/{orderId}/items/{itemId}", getOrderItem)

В OpenAPI-спеке — уникально всегда:

/api/v1/orders/{orderId}/items/{itemId}:
  get:
    operationId: getOrderItem
    parameters:
      - name: orderId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: itemId
        in: path
        required: true
        schema:
          type: string
          format: uuid

Swagger UI/Redoc не рендерят операцию корректно, если два параметра в одном пути называются одинаково — требование инструмента, не семантическое.

summary и description

R-OAS-4: summary — обязателен, до 80 символов; description — только если логика неочевидна.

// getSberCardLimit godoc
// @Summary      Получить лимит карты Сбера
// @Description  Возвращает текущий расходный лимит по карте клиента.
//               Лимит пересчитывается ночью; в течение дня — кешированное значение.
// @Tags         Customers
// @Param        customerId path string true "ID клиента"
// @Success      200 {object} CardLimitResponse
// @Router       /api/v1/customers/{customerId}/card-limit [get]
// @operationId  getCustomerCardLimit
func getCustomerCardLimit(w http.ResponseWriter, r *http.Request) { ... }

Пустой description лучше, чем дублирование summary. Description нужен когда: есть побочные эффекты, поведение зависит от роли, или семантика неочевидна из названия.

Структуры Go как источник контракта

В Go нет reflection-based OpenAPI-генерации уровня Spring или FastAPI. Два варианта синхронизации:

Вариант А — swaggo/swag: аннотации в комментариях, swag init генерирует docs/swagger.json. Плюс: спека всегда рядом с кодом. Минус: аннотации дублируют структуры.

Вариант Б — ручная спека: openapi.yaml рядом с main.go. Структуры Go — источник правды; ревьюер проверяет соответствие до мержа.

В обоих вариантах структуры response-типов определяют контракт:

type OrderResponse struct {
    OrderID    string     `json:"orderId"`
    Status     string     `json:"status"`      // NEW | CONFIRMED | SHIPPED
    CreatedAt  time.Time  `json:"createdAt"`
    TotalRub   int64      `json:"totalRub"`    // копейки
    Items      []ItemResponse `json:"items"`
    Note       string     `json:"note,omitempty"`  // omitempty — поле отсутствует если пусто
}

type PageResponse[T any] struct {
    Content []T `json:"content"`
    Page    int `json:"page"`
    Size    int `json:"size"`
    Total   int `json:"total"`
}

omitempty на Note означает: поле отсутствует в JSON, когда пусто — нет "note": null, нет "note": "". Это одновременно закрывает R-RSP-X1 и R-RSP-X2.

Антипаттерны: сводная таблица

Полный чеклист для ревью — все X-коды контракта в идиомах Go.

URL и маршрутизация

АнтипаттернПравилоЧто взамен
Глагол в URL для CRUD (/getOrders, /createProduct)R-URL-X4GET /orders, POST /products
CamelCase или snake_case в пути (/orderItems, /order_items)R-URL-X1/order-items
Trailing slash в chi (r.Get("/orders/", ...))R-URL-X2r.Get("/orders", ...)
Расширение в пути (/orders.json)R-URL-X3/orders + Accept: application/json
ID в теле запроса вместо chi.URLParamR-NEST-X2orderId := chi.URLParam(r, "orderId")
Три уровня вложенности (/orders/{id}/items/{id}/shipments)R-NEST-X1GET /shipments?itemId={id}
Единственное число коллекции (/order, /product)R-RES-X1/orders, /products
GET с побочным эффектом (GET /orders/{id}/confirm)R-MTH-X1POST /orders/{id}/confirm

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

АнтипаттернПравилоЧто взамен
Версия в query (?version=2)R-VER-X2r.Route("/api/v2", ...)
Минорная версия (/api/v1.2/)R-VER-X1/api/v2/ только при breaking change
Эндпоинт без /api или без версииR-VER-X3r.Route("/api/v1", ...) обязательно
Новая версия ради необязательного поляR-VER-X4добавить omitempty-поле в текущую

Query-параметры

АнтипаттернПравилоЧто взамен
Comma-separated массивы (?status=NEW,PAID)R-QRY-X3r.Form["status"] — повтор параметра
page=0 или 0-based нумерацияR-QRY-X2page >= 1; дефолт — Page: 1
snake_case или PascalCase в параметрахR-QRY-X1camelCase: createdFrom, pageSize
Бизнес-логика в query (?action=cancel)R-QRY-X4POST /orders/{id}/cancel
Конструирование cursor на клиентеR-QRY-X5cursor — непрозрачный токен, base64 или UUID

JSON и ответы

АнтипаттернПравилоЧто взамен
*string-указатель в response-структуре → null в 2xxR-RSP-X1string + omitempty; *string только в PATCH-запросе
json.Marshal без omitempty на необязательных поляхR-RSP-X1добавить omitempty или не включать поле
Пустая строка вместо отсутствия поля ("note": "")R-RSP-X2string с omitempty
Envelope {"data": {...}, "success": true}R-RSP-X4плоский объект без обёртки
"content": null для пустой коллекцииR-RSP-7"content": [] — инициализировать слайс
float64 для денежных суммint64 (минорные единицы — копейки, центы)

Заголовки

АнтипаттернПравилоЧто взамен
X--prefix в кастомных заголовках (X-Request-Id)R-HDR-X1доменный префикс (Shop-Request-Id)

Ошибки

АнтипаттернПравилоЧто взамен
Content-Type: application/json для ошибокR-ERR-X1application/problem+json
type: "about:blank" в ProblemDetailsR-ERR-X2urn:problem:order-service:not-found
HTTP 422 для ошибок валидацииR-ERR-X3400 Bad Request + code: VALIDATION_ERROR
err.Error() из pgx/sqlc в поле detailR-ERR-X4санитизировать через apperr.KindOf; в detail — общая фраза
Разные структуры ошибок в разных хендлерахR-PRIN-2единый httperr.Write(w, r, err) через весь сервис

Rate limiting и deprecation

АнтипаттернПравилоЧто взамен
429 без Retry-After и RateLimit-*R-RATE-X1middleware выставляет все три заголовка
deprecated: true в OpenAPI без SunsetR-DEP-X1middleware с Sunset + Deprecation + Link

Alias и actions

АнтипаттернПравилоЧто взамен
me для собственных ресурсов singleton (/users/me/settings)R-ALIAS-X1/settings — контекст из токена
HATEOAS-ссылки в теле ответаR-PRIN-X1OpenAPI описывает навигацию; только Location при создании
Существительное в action (/cancellation, /confirmation)R-ACT-X1/cancel, /confirm — глагол-инфинитив
PUT или PATCH для action-эндпоинтаR-ACT-X2POST /orders/{id}/confirm

OpenAPI-метаданные

АнтипаттернПравилоЧто взамен
Нет operationId или дублированиеR-OAS-1уникальный camelCase на каждой операции
Action-эндпоинт в отдельном теге (OrderActions)R-OAS-2тег родительского ресурса (Orders)
Одинаковые имена параметров в одном пути в OpenAPIR-OAS-3{orderId}, {itemId} — уникально
Нет summary на операцииR-OAS-4короткая фраза до 80 символов — обязательно

Куда дальше

  • go/url-and-resources.md — kebab-case, вложенность, chi.URLParam
  • go/alias-and-actions.md — me, action-эндпоинты, POST /confirm
  • go/versioning.md — breaking change, chi r.Route("/api/v2", ...)
  • go/query-params.md — camelCase, r.Form["status"], cursor
  • go/json-and-responses.md — omitempty, PageResponse, int64
  • go/errors.md — ProblemDetails, httperr.Write, violations
  • go/headers.md — Idempotency-Key, traceparent, доменный префикс
  • go/rate-limiting-files-deprecation.md — 429, Sunset, multipart
  • go/batch-async-localization.md — batch, 202 Accepted, Accept-Language
  • Ошибки — error-handling Goapperr.Kind, errors.As