Опирается на правила:
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-X1 | OpenAPI описывает навигацию |
/OrderItems (заглавные) | R-URL-X1 | /order-items |
redirect_slashes=True (дефолт) | R-URL-X2 | redirect_slashes=False |
/api/v1/orders.json | R-URL-X3 | Accept: 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-X1 | flat с filter |
| ID в body вместо path | R-NEST-X2 | path-параметр |
Куда дальше
- REST API → URL и ресурсы (нормативно) — формулировки.
- Alias и Action-эндпоинты —
me,latest, доменные команды. - Версионирование —
APIRouter(prefix="/api/v1"), v1/v2. - Query-параметры и пагинация — фильтры,
Query(alias=...). - OpenAPI и антипаттерны —
operation_id,tags.