← Back to the section

CRUD works great for simple operations: create, read, update, delete. But some operations don't fit that scheme. How do you get the current user's profile without knowing their ID? How do you signal that an order has been confirmed, rather than just that a status field changed? Let's look at two FastAPI tools for cases like these.

Alias Segments — Shortcuts in the URL

Why you need me

To get a specific user's profile, you usually do:

GET /users/{user_id}

But a user doesn't always know their own ID. They just want to view their profile. That's exactly what the me alias is for:

GET /users/me

The server reads the ID from the authorization token and returns the right profile.

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

An important nuance about route ordering. FastAPI matches routes strictly in the order they were registered. If /users/{user_id} is registered first, then a GET /users/me request will hit it, and user_id will get the value "me" — which will most likely cause an error. Literal segments (/me, /latest) must be registered before parametric ones (/{user_id}).

router = APIRouter()
router.include_router(users_me_router)   # literal — first
router.include_router(users_id_router)  # parametric — second

When you need me and when you don't

A simple rule: can an administrator hit the same endpoint with someone else's ID?

  • Yes — you need both variants (/users/me for a regular user and /users/{user_id} for an administrator).
  • No — the endpoint already only works for the current user, so me is redundant.

A common mistake is adding me where it isn't needed:

# Wrong: orders are already taken from the token
@router.get("/users/me/orders")

# Right: /orders returns the current user's orders
@router.get("/orders")

Another mistake is dropping users/ from the path:

# Wrong: me is shorthand for users/{id}
@router.get("/me")

# Right
@router.get("/users/me")

Alias for "latest", "current", "next"

Sometimes you need not a specific resource by ID, but the "latest" or the "current" one. For that, temporal aliases are used:

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

The ordering rule is the same: /versions/latest is registered before /versions/{version_id}.

Alias by a business attribute

When a user can have several objects of the same type, but one of them is the "main" one:

@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 Endpoints — Domain Commands

The problem with PATCH for complex operations

It's tempting to update the status field via PATCH:

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

But this approach has a hidden problem: PATCH describes a change of data, not a business operation. Confirming an order is not just changing a field. It may mean: charge money, send a notification, put it in the assembly queue. With PATCH, that semantics is visible neither from the URL nor from the logs.

An action endpoint makes the intent explicit:

POST /orders/{order_id}/confirm

From the log it's immediately clear what happened.

How to build an action endpoint

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

A few important details:

  • The action name is a verb in the infinitive: confirm, cancel, ship. Not confirmation, not confirmed.
  • The method is always POST, even if the operation is idempotent. An action is a command, not a data query.
  • status_code=200, not 201: an action doesn't create a new resource, it returns the updated state of an existing one.
  • The request body is a Pydantic model, if parameters are needed. If there are no parameters, the body is optional.

When action, and when PATCH

# A simple field change with no business rules — PATCH
@router.patch("/orders/{order_id}")
async def patch_order(order_id: str, body: PatchOrderRequest): ...

# A command with domain semantics — action
@router.post("/orders/{order_id}/confirm")
async def confirm_order(order_id: str): ...

A guideline for choosing:

SituationWhat to use
Changing name, descriptionPATCH
Transitioning to a new status (state machine)Action
The operation has a domain nameAction
Side effects: events, payments, notificationsAction
A simple field with no side effectsPATCH

Common mistakes with action endpoints

Using a noun or a participle instead of a verb in the infinitive:

# Wrong
@router.post("/orders/{order_id}/confirmation")
@router.post("/orders/{order_id}/confirmed")

# Right
@router.post("/orders/{order_id}/confirm")

Using PUT or PATCH for an action:

# Wrong
@router.put("/orders/{order_id}/confirm")

# Right
@router.post("/orders/{order_id}/confirm")

In short

  • me — an alias for the current user instead of an explicit ID. Needed only when the same endpoint can accept someone else's ID (for example, for an administrator).
  • Route ordering in FastAPI is critical: literal ones (/me, /latest) are registered before parametric ones (/{user_id}).
  • latest, current, default, primary — aliases for a singleton selection by a temporal or business attribute.
  • /me without users/ is a mistake: me is shorthand for users/{id}, so the path must be /users/me.
  • Action endpoints — for domain commands with semantics: POST /orders/{id}/confirm, POST /orders/{id}/cancel.
  • The action name is a verb in the infinitive (confirm, not confirmation), the method is POST, the status is 200.
  • PATCH is for changing data, an action is for domain operations with side effects.
  • URLs and resources in FastAPI — path format, kebab-case.
  • JSON and response format — response_model_exclude_none=True.
  • OpenAPI and anti-patterns — operation_id, tags for actions.
  • API versioning — actions in v2.