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/mefor a regular user and/users/{user_id}for an administrator). - No — the endpoint already only works for the current user, so
meis 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. Notconfirmation, notconfirmed. - 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:
| Situation | What to use |
|---|---|
Changing name, description | PATCH |
| Transitioning to a new status (state machine) | Action |
| The operation has a domain name | Action |
| Side effects: events, payments, notifications | Action |
| A simple field with no side effects | PATCH |
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./mewithoutusers/is a mistake:meis shorthand forusers/{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, notconfirmation), the method isPOST, the status is200. - PATCH is for changing data, an action is for domain operations with side effects.
What to read next
- URLs and resources in FastAPI — path format, kebab-case.
- JSON and response format —
response_model_exclude_none=True. - OpenAPI and anti-patterns —
operation_id,tagsfor actions. - API versioning — actions in v2.