← назад к разделу

CRUD отлично работает для простых операций: создать, прочитать, обновить, удалить. Но некоторые операции не укладываются в эту схему. Как получить профиль текущего пользователя без знания его ID? Как обозначить, что заказ подтверждён, а не просто поменялось поле status? Разберём два инструмента FastAPI для таких случаев.

Alias-сегменты — shortcuts в URL

Зачем нужен me

Чтобы получить профиль конкретного пользователя, обычно делают:

GET /users/{user_id}

Но пользователь не всегда знает свой ID. Он просто хочет посмотреть свой профиль. Для этого и нужен alias me:

GET /users/me

Сервер читает ID из токена авторизации и возвращает нужный профиль.

from fastapi import APIRouter, Depends
from .auth import get_current_user

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

@router.get("/users/me")
async def get_current_user_profile(current_user = Depends(get_current_user)):
    ...

@router.get("/users/{user_id}")
async def get_user(user_id: str):
    ...

Важный нюанс с порядком маршрутов. FastAPI сопоставляет маршруты строго в порядке регистрации. Если /users/{user_id} зарегистрирован первым, то запрос GET /users/me попадёт туда, и user_id получит значение "me" — что, скорее всего, вызовет ошибку. Литеральные сегменты (/me, /latest) должны регистрироваться до параметрических (/{user_id}).

router = APIRouter()
router.include_router(users_me_router)   # литеральный — первым
router.include_router(users_id_router)  # параметрический — вторым

Когда me нужен, а когда нет

Простое правило: может ли администратор обратиться к этому же эндпоинту по чужому ID?

  • Да — нужны оба варианта (/users/me для обычного пользователя и /users/{user_id} для администратора).
  • Нет — эндпоинт и так работает только для текущего пользователя, me избыточен.

Частая ошибка — добавлять me туда, где он не нужен:

# Неверно: заказы и так берутся из токена
@router.get("/users/me/orders")

# Верно: /orders возвращает заказы текущего пользователя
@router.get("/orders")

Ещё одна ошибка — убирать users/ из пути:

# Неверно: me — это сокращение для users/{id}
@router.get("/me")

# Верно
@router.get("/users/me")

Alias для «последнего», «текущего», «следующего»

Иногда нужно получить не конкретный ресурс по ID, а «последний» или «текущий». Для этого используют временны́е alias:

@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 по бизнес-признаку

Когда у пользователя может быть несколько объектов одного типа, но один из них «главный»:

@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)):
    ...

Action-эндпоинты — доменные команды

Проблема с PATCH для сложных операций

Есть соблазн обновлять поле status через PATCH:

PATCH /orders/{order_id}
{"status": "CONFIRMED"}

Но у этого подхода есть скрытая проблема: PATCH описывает изменение данных, а не бизнес-операцию. Подтверждение заказа — это не просто смена поля. Это может означать: списать деньги, отправить уведомление, поставить в очередь на сборку. Через PATCH такая семантика не видна ни из URL, ни из логов.

Action-эндпоинт делает намерение явным:

POST /orders/{order_id}/confirm

Из лога сразу понятно, что произошло.

Как оформить action-эндпоинт

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,
    response_model=OrderResponse,
)
async def confirm_order(order_id: str) -> OrderResponse:
    ...

@router.post(
    "/orders/{order_id}/cancel",
    status_code=200,
    response_model=OrderResponse,
)
async def cancel_order(order_id: str) -> OrderResponse:
    ...

@router.post(
    "/orders/{order_id}/ship",
    status_code=200,
    response_model=OrderResponse,
)
async def ship_order(order_id: str, body: ShipOrderRequest) -> OrderResponse:
    ...

Несколько важных деталей:

  • Имя action — глагол в инфинитиве: confirm, cancel, ship. Не confirmation, не confirmed.
  • Метод всегда POST, даже если операция идемпотентна. Action — это команда, а не запрос данных.
  • status_code=200, не 201: action не создаёт новый ресурс, а возвращает обновлённое состояние существующего.
  • Тело запроса — Pydantic-модель, если нужны параметры. Если параметров нет — тело опционально.

Когда action, а когда PATCH

# Простое изменение поля без бизнес-правил — PATCH
@router.patch("/orders/{order_id}")
async def patch_order(order_id: str, body: PatchOrderRequest): ...

# Команда с доменной семантикой — action
@router.post("/orders/{order_id}/confirm")
async def confirm_order(order_id: str): ...

Ориентир для выбора:

СитуацияЧто использовать
Меняем name, descriptionPATCH
Переводим в новый статус (конечный автомат)Action
Операция имеет доменное имяAction
Побочные эффекты: события, платежи, уведомленияAction
Простое поле без побочных эффектовPATCH

Частые ошибки с action-эндпоинтами

Использование существительного или причастия вместо глагола в инфинитиве:

# Неверно
@router.post("/orders/{order_id}/confirmation")
@router.post("/orders/{order_id}/confirmed")

# Верно
@router.post("/orders/{order_id}/confirm")

Использование PUT или PATCH для action:

# Неверно
@router.put("/orders/{order_id}/confirm")

# Верно
@router.post("/orders/{order_id}/confirm")

Коротко

  • me — alias для текущего пользователя вместо явного ID. Нужен только когда тот же эндпоинт может принять чужой ID (например, для администратора).
  • Порядок маршрутов в FastAPI критичен: литеральные (/me, /latest) регистрируются до параметрических (/{user_id}).
  • latest, current, default, primary — alias для singleton-выборки по временно́му или бизнес-признаку.
  • /me без users/ — ошибка: me — это сокращение для users/{id}, путь должен быть /users/me.
  • Action-эндпоинты — для доменных команд с семантикой: POST /orders/{id}/confirm, POST /orders/{id}/cancel.
  • Имя action — глагол в инфинитиве (confirm, не confirmation), метод — POST, статус — 200.
  • PATCH — для изменения данных, action — для доменных операций с побочными эффектами.

Что почитать дальше

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