Опирается на правила: 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, descriptionPATCH
Меняем status (state machine)Action
Команда с доменным именемAction
Бизнес-правила, событияAction
Простое поле без эффектовPATCH

Action делает семантику явной — в логах видно POST /orders/{id}/confirm, не PATCH /orders/{id}.

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

АнтипаттернПравилоЧто взамен
me для singleton endpointR-ALIAS-X1без me (singleton сам)
@router.get("/me") без users/R-ALIAS-X2/users/me
/orders/{id}/confirmationR-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-1POST /orders/{id}/cancel
Параметрический маршрут до литерального(FastAPI routing)литеральные регистрируются первыми
Многословный action cancel_the_order_nowR-ACT-2cancel

Куда дальше

  • REST API → Alias и Action (нормативно) — формулировки.
  • URL и ресурсы — формат пути, kebab-case.
  • JSON и формат ответов — response_model_exclude_none=True.
  • OpenAPI и антипаттерны — operation_id, tags для action.
  • Версионирование — action в v2.