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, description | PATCH |
| Переводим в новый статус (конечный автомат) | 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.