Опирается на правила: R-PRIN-1..4, R-URL-1..3, R-RES-1..3, R-MTH-1..6, R-NEST-1..4 из REST API Style Guide → раздел URL и ресурсы.

Важно знать

  • 4 принципа: предсказуемость, единообразие, читаемость, стабильность.
  • URL — часть публичного контракта, изменение = breaking change.
  • HATEOAS-ссылки в теле запрещены, навигация — в OpenAPI.
  • kebab-case, lowercase, без trailing slash (redirect_slashes=False).
  • Коллекции — множественное число (/orders); singleton — единственное (/profile).
  • Метод = декоратор: @router.get, @router.post, status_code=201.
  • 2 уровня вложенности максимум. Глубже — flat resource с filter.
  • Path-параметр в FastAPI — уникальный ({order_id}), в дизайне URL — {id}.

REST URL — первая документация API. FastAPI строит OpenAPI из декораторов маршрутов и Pydantic-моделей: то, что написано в @router.get("/orders/{order_id}/items"), напрямую попадает в /openapi.json. Ошибки в именовании закрепляются в сгенерированной спеке.

Принципы

R-PRIN-1..4:

  • Предсказуемость — знающий один маршрут угадывает остальные.
  • Единообразие — одни правила для всех роутеров.
  • ЧитаемостьGET /orders/{order_id}/items = «get order's items».
  • Стабильность — URL — публичный контракт, изменение = breaking change.

R-PRIN-X1: HATEOAS-ссылки запрещены. Поле _links или href в теле ответа — не добавляем. Единственное исключение — заголовок Location при создании ресурса.

Формат пути в FastAPI

R-URL-1..3:

from fastapi import APIRouter

router = APIRouter(prefix="/api/v1", redirect_slashes=False)

@router.get("/orders/{order_id}/items")         # kebab-case, без слеша
async def get_order_items(order_id: str): ...

@router.get("/order-items")                     # kebab-case в сегменте
async def get_order_items_flat(): ...
/api/v1/order-items                 ✓
/api/v1/delivery-addresses          ✓
/api/v1/users/{order_id}/profile    ✓

/api/v1/OrderItems                  ✗ — заглавные
/api/v1/order_items                 ✗ — snake_case в URL
/api/v1/deliveryAddresses           ✗ — camelCase
/api/v1/orders/                     ✗ — trailing slash
/api/v1/orders.json                 ✗ — расширение

redirect_slashes=Falseобязательно. По умолчанию FastAPI возвращает 307 Temporary Redirect на URL со слешем, что маскирует ошибку клиента.

Служебные эндпоинты — вне /api/v1:

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
async def health(): return {"status": "ok"}

@app.get("/ready")
async def ready(): return {"status": "ok"}

/health, /ready, /metrics — не версионируются, не требуют аутентификации.

Ресурсы

R-RES-1..3:

router = APIRouter(prefix="/api/v1", redirect_slashes=False)

@router.get("/orders")                      # коллекция — множественное число
@router.get("/orders/{order_id}")           # один заказ
@router.get("/users/{user_id}/profile")     # singleton
/orders                             ✓ коллекция
/orders/{order_id}                  ✓ один заказ
/users/{user_id}/profile            ✓ singleton

/order                              ✗ единственное для коллекции
/orders/{order_id}/item             ✗ mix единственного и множественного

Имя ресурса = доменный термин из Ubiquitous Language:

Order    → /orders
Product  → /products
Customer → /customers

Не /purchases, не /transactions, не /clients. Трассируемость кода, спеки и URL.

HTTP-методы

R-MTH-1..6: декоратор = метод.

@router.get("/orders", status_code=200)
async def get_orders(): ...

@router.post("/orders", status_code=201)
async def create_order(): ...

@router.put("/orders/{order_id}", status_code=200)
async def update_order(order_id: str): ...

@router.patch("/orders/{order_id}", status_code=200)
async def patch_order(order_id: str): ...

@router.delete("/orders/{order_id}", status_code=204)
async def delete_order(order_id: str): ...
ДекораторСемантикаИдемпотентныйSuccess
@router.getчтениеда200
@router.postсоздание / команданет201
@router.putполная заменада200
@router.patchчастичное обновлениеда на практике200
@router.deleteудалениеда204
@router.post("/orders/{order_id}/cancel")        # ✓ POST для команды с side-effect
async def cancel_order(order_id: str): ...

@router.get("/orders/{order_id}/cancel")         # ✗ GET с побочным эффектом
async def cancel_order(order_id: str): ...

Вложенность

R-NEST-1..4:

@router.get("/orders/{order_id}/items")           # 2 уровня — OK
@router.get("/orders/{order_id}/items/{item_id}") # 2 уровня + id — OK

# ✗ 3 уровня — запрещено
# /users/{user_id}/orders/{order_id}/items/{item_id}

Глубже двух уровней — flat resource с filter:

@router.get("/items")
async def get_items(order_id: str = Query(alias="orderId")): ...

Path-параметр: дизайн vs OpenAPI

В дизайне URL — всегда {id}:

/orders/{id}/items/{id}

В FastAPI-коде и OpenAPI — уникальные имена:

@router.get("/orders/{order_id}/items/{item_id}")
async def get_order_item(order_id: str, item_id: str): ...

Swagger UI и Redoc не работают с дублирующимися именами параметров пути. Это требование инструмента, не семантика.

Что запрещено

АнтипаттернПравилоЧто взамен
HATEOAS _links в теле ответаR-PRIN-X1OpenAPI описывает навигацию
/OrderItems (заглавные)R-URL-X1/order-items
redirect_slashes=True (дефолт)R-URL-X2redirect_slashes=False
/api/v1/orders.jsonR-URL-X3Accept: application/json
@router.get("/getOrders") (глагол)R-URL-X4@router.get("/orders")
@router.get("/order") для коллекцииR-RES-X1/orders
/order/{id}/items mix ед./мн.R-RES-X2/orders/{id}/items
@router.get с побочным эффектомR-MTH-X1@router.post
3 уровня вложенностиR-NEST-X1flat с filter
ID в body вместо pathR-NEST-X2path-параметр

Куда дальше

  • REST API → URL и ресурсы (нормативно) — формулировки.
  • Alias и Action-эндпоинты — me, latest, доменные команды.
  • Версионирование — APIRouter(prefix="/api/v1"), v1/v2.
  • Query-параметры и пагинация — фильтры, Query(alias=...).
  • OpenAPI и антипаттерны — operation_id, tags.