Опирается на правила:
R-URL-1..4,R-URL-X1..X4,R-RES-1..3,R-RES-X1..X2,R-NEST-1..4,R-NEST-X1..X3→ раздел URL и ресурсы.
Важно знать
- Только строчные буквы, разделитель слов — дефис (
kebab-case):/sales-orders,/payment-methods.- chi не редиректит trailing slash по умолчанию — не добавляй его в маршруты.
- Коллекция — существительное во множественном числе:
/orders,/products,/customers.- Singleton в контексте родителя — единственное число:
/orders/{id}/summary.- Максимум два уровня вложенности; третий уровень — ресурс на верхний уровень с фильтром.
- Path-параметр в дизайне —
{id}; в chi читается черезchi.URLParam(r, "id").- В OpenAPI параметры именуются уникально:
{orderId},{itemId}.- Служебные эндпоинты (
/health,/metrics) — вне/api/v1.
Структура маршрутов в chi
R-URL-1..3 и R-NEST-1:
r := chi.NewRouter()
r.Get("/health", healthHandler) // вне /api/v1 (R-URL-3)
r.Route("/api/v1", func(r chi.Router) {
r.Route("/orders", func(r chi.Router) {
r.Get("/", listOrders)
r.Post("/", createOrder)
r.Post("/search", searchOrders)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getOrder)
r.Put("/", updateOrder)
r.Patch("/", patchOrder)
r.Delete("/", deleteOrder)
r.Post("/confirm", confirmOrder)
r.Post("/cancel", cancelOrder)
r.Route("/items", func(r chi.Router) { // два уровня — ок
r.Get("/", listOrderItems)
r.Post("/", addOrderItem)
})
})
})
r.Route("/products", func(r chi.Router) {
r.Get("/", listProducts)
r.Post("/", createProduct)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getProduct)
r.Put("/", updateProduct)
r.Delete("/", deleteProduct)
})
})
})
Чтение path-параметра
R-NEST-4: в дизайне URL используется {id}, контекст из сегмента устраняет неоднозначность.
func getOrder(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
order, err := svc.GetOrder(r.Context(), id)
if err != nil {
httperr.Write(w, r, err)
return
}
writeJSON(w, http.StatusOK, toOrderResponse(order))
}
func getOrderItem(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "id") // в разных уровнях r.Route — разные контексты
// ...
}
Вложенный /orders/{id}/items читает chi.URLParam из контекста правильно — chi пишет параметры в контекст послойно.
Два уровня вложенности
R-NEST-1: третий уровень выносится наверх с фильтром.
// два уровня — ок
r.Get("/orders/{id}/items", listOrderItems)
// три уровня — нарушение R-NEST-X1
// r.Get("/orders/{id}/items/{id}/shipments", ...) ✗
// вместо этого:
r.Get("/shipments", listShipments) // ?itemId=...
func listShipments(w http.ResponseWriter, r *http.Request) {
itemID := r.URL.Query().Get("itemId")
// ...
}
Именование ресурсов
R-RES-1..3: доменные термины, множественное число для коллекций.
// коллекции — множественное число
r.Route("/orders", ...) // ✓
r.Route("/products", ...) // ✓
r.Route("/customers", ...) // ✓
r.Route("/payment-methods", .) // ✓ kebab-case для двух слов
// singleton в контексте родителя — единственное число
r.Get("/orders/{id}/summary", getOrderSummary) // ✓
r.Get("/customers/{id}/profile", getCustomerProfile) // ✓
Запросы, не укладывающиеся в CRUD
Для сложной фильтрации — POST /resources/search с JSON-телом (R-QRY-9):
r.Post("/orders/search", searchOrders)
type SearchOrdersRequest struct {
CustomerID string `json:"customerId"`
Statuses []string `json:"statuses"`
AmountFrom int64 `json:"amountFrom"`
AmountTo int64 `json:"amountTo"`
}
func searchOrders(w http.ResponseWriter, r *http.Request) {
var req SearchOrdersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperr.Write(w, r, apperr.NewValidation("invalid request body"))
return
}
// ...
}
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
/Orders, /salesOrders, /sales_orders | R-URL-X1 | /orders, /sales-orders |
/api/v1/orders/ (trailing slash) | R-URL-X2 | /api/v1/orders |
/api/v1/orders.json | R-URL-X3 | /api/v1/orders + Accept: application/json |
/api/v1/getOrders, /api/v1/createOrder | R-URL-X4 | GET /api/v1/orders, POST /api/v1/orders |
/api/v1/order (единственное число) | R-RES-X1 | /api/v1/orders |
/api/v1/orders/{id}/items/{id}/shipments | R-NEST-X1 | /api/v1/shipments?itemId= |
| ID родителя в теле вместо пути | R-NEST-X2 | POST /api/v1/orders/{id}/items |
/api/v1/orders/{orderId}/items/{itemId} в дизайне | R-NEST-X3 | {id} в дизайне, уникальные имена только в OpenAPI |
Куда дальше
- go/versioning.md —
/api/v1prefix, breaking change. - go/query-params.md — параметры фильтрации и поиска.
- go/alias-and-actions.md —
me,latest, action-эндпоинты. - go/json-and-responses.md — формат тела ответа.
- go/errors.md — коды ошибок при неверном URL.