Опирается на правила:
R-ALIAS-1..3,R-ACT-1..4и X-коды из REST API Style Guide → раздел Alias и Action-эндпоинты.
Важно знать
me— только когда endpoint может принять и свой, и чужой ID.- Граница
me: «может ли супер-админ обратиться по другому ID?» Да →meнужен.- Порядок маршрутов FastAPI критичен: литеральные сегменты (
/me,/latest) — до параметрических (/{user_id}).- Временные alias (
latest,current) и логические (default,primary) — для singleton-выборки.- Action-эндпоинты —
@router.post("/orders/{order_id}/confirm").- Имя action — глагол в инфинитиве (
confirm, неconfirmation).- Метод action — всегда
POST,status_code=200, тело — обновлённый ресурс./api/v1/meбезusers/— запрещён.
CRUD не покрывает все доменные операции. FastAPI раскрывает два расширения: alias-сегменты для shortcut-выборки и action-эндпоинты для команд, меняющих состояние агрегата.
Alias-сегменты
me
R-ALIAS-1: shortcut вместо явного идентификатора.
from fastapi import APIRouter, Depends
from .auth import get_current_user
router = APIRouter(prefix="/api/v1", redirect_slashes=False)
@router.get("/users/{user_id}")
async def get_user(user_id: str):
...
@router.get("/users/me")
async def get_current_user_profile(current_user = Depends(get_current_user)):
...
FastAPI сопоставляет маршруты в порядке регистрации. Маршрут /users/me должен быть зарегистрирован до /users/{user_id}, иначе FastAPI трактует me как значение параметра user_id.
router = APIRouter()
router.include_router(users_me_router) # литеральный — первым
router.include_router(users_id_router) # параметрический — вторым
Граничный тест: может ли супер-админ обратиться по другому ID к этому же endpoint?
- Да →
meнужен (GET /users/{user_id}для admin,GET /users/meдля обычного user). - Нет → endpoint singleton,
meизбыточен.
Временные и порядковые alias
R-ALIAS-2: для singleton-выборки.
@router.get("/products/{product_id}/versions/latest")
async def get_latest_version(product_id: str):
...
@router.get("/orders/current")
async def get_current_order(current_user = Depends(get_current_user)):
...
@router.get("/invoices/next")
async def get_next_invoice():
...
Порядок в роутере: /versions/latest до /versions/{version_id}.
Логические alias
R-ALIAS-3: по бизнес-признаку.
@router.get("/payment-methods/default")
async def get_default_payment_method(current_user = Depends(get_current_user)):
...
@router.get("/addresses/primary")
async def get_primary_address(current_user = Depends(get_current_user)):
...
@router.get("/subscriptions/active")
async def get_active_subscription(current_user = Depends(get_current_user)):
...
Запреты для me
R-ALIAS-X1: me там, где endpoint singleton по природе.
@router.get("/users/me/orders") # ✗ — orders уже из токена
@router.get("/orders") # ✓ — заказы текущего пользователя (из токена)
R-ALIAS-X2: me без users/.
@router.get("/me") # ✗ — me — alias для users/{id}
@router.get("/users/me") # ✓
Action-эндпоинты
Доменные команды, меняющие состояние агрегата.
Формат
R-ACT-1..4:
from pydantic import BaseModel
class ShipOrderRequest(BaseModel):
tracking_number: str
carrier: str
class OrderResponse(BaseModel):
order_id: str
status: str
...
@router.post(
"/orders/{order_id}/confirm",
status_code=200,
operation_id="confirmOrder",
tags=["Orders"],
summary="Подтвердить заказ",
response_model=OrderResponse,
response_model_exclude_none=True,
)
async def confirm_order(order_id: str) -> OrderResponse:
...
@router.post(
"/orders/{order_id}/cancel",
status_code=200,
operation_id="cancelOrder",
tags=["Orders"],
summary="Отменить заказ",
response_model=OrderResponse,
response_model_exclude_none=True,
)
async def cancel_order(order_id: str) -> OrderResponse:
...
@router.post(
"/orders/{order_id}/ship",
status_code=200,
operation_id="shipOrder",
tags=["Orders"],
summary="Передать заказ в доставку",
response_model=OrderResponse,
response_model_exclude_none=True,
)
async def ship_order(order_id: str, body: ShipOrderRequest) -> OrderResponse:
...
- Имя — глагол в инфинитиве (
confirm,cancel,ship, неconfirmation). - Метод —
@router.postвсегда, даже если операция идемпотентна. status_code=200— action не создаёт ресурс, возвращает обновлённый.- Body — Pydantic-модель запроса; если параметров нет — тело опционально.
Когда action vs PATCH
# Меняем поле, нет бизнес-правил — PATCH
@router.patch("/orders/{order_id}", ...)
async def patch_order(order_id: str, body: PatchOrderRequest): ...
# Команда с side-effects (события, state machine) — action
@router.post("/orders/{order_id}/confirm", ...)
async def confirm_order(order_id: str): ...
| Ситуация | Что выбрать |
|---|---|
Меняем name, description | PATCH |
Меняем status (state machine) | Action |
| Команда с доменным именем | Action |
| Бизнес-правила, события | Action |
| Простое поле без эффектов | PATCH |
Action делает семантику явной — в логах видно POST /orders/{id}/confirm, не PATCH /orders/{id}.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
me для singleton endpoint | R-ALIAS-X1 | без me (singleton сам) |
@router.get("/me") без users/ | R-ALIAS-X2 | /users/me |
/orders/{id}/confirmation | R-ACT-X1 | /orders/{id}/confirm |
/orders/{id}/confirmed (причастие) | R-ACT-X1 | /orders/{id}/confirm |
@router.put("/orders/{id}/confirm") | R-ACT-X2 | @router.post |
PATCH /orders/{id} {status: CANCELLED} для команды | R-ACT-1 | POST /orders/{id}/cancel |
| Параметрический маршрут до литерального | (FastAPI routing) | литеральные регистрируются первыми |
Многословный action cancel_the_order_now | R-ACT-2 | cancel |
Куда дальше
- REST API → Alias и Action (нормативно) — формулировки.
- URL и ресурсы — формат пути, kebab-case.
- JSON и формат ответов —
response_model_exclude_none=True. - OpenAPI и антипаттерны —
operation_id,tagsдля action. - Версионирование — action в v2.