← Python Code FastAPI
Browse Python Concepts

FastAPI Project Structure — Route Shadowing

Mental Model

Think of FastAPI's router as a waterfall: it tries to match incoming requests from top to bottom based on the order routes are registered. The first pattern that matches 'catches' the request, regardless of specificity.

Rule: Always register static and literal path endpoints before dynamic path parameters within your routing definition files.

The Setup

In a clean FastAPI project, endpoints are split across multiple files. During refactoring, you split user-related routes into an APIRouter. One endpoint fetches a specific user by ID, while another fetches the currently authenticated user session.

What Does This Print?

Broken code
Python
from fastapi import FastAPI, APIRouter

router = APIRouter()

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

@router.get("/users/me")
async def get_current_user():
    return {"user_id": "authenticated_user", "source": "static"}

app = FastAPI()
app.include_router(router)
Predict what payload is returned when a client sends a GET request to /users/me.

The Output

What actually happens
{ "user_id": "me", "source": "dynamic" }

The request returns a 200 OK status code but with the wrong payload: Instead of routing the request to get_current_user, FastAPI matched /users/me against the path pattern /users/{user_id}, assigning the string value "me" to the path parameter user_id.

Why Python Does This

FastAPI builds its routing table on top of Starlette's Router. Starlette matches incoming requests against path operations sequentially in the exact order they are registered. Since Python executes module code from top to bottom, the decorator @router.get("/users/{user_id}") registers its route first. When an HTTP request arrives, Starlette's routing engine evaluates the list of registered patterns one by one. Because "me" matches the regular expression generated for {user_id} (which defaults to a string match pattern), execution enters the first matching endpoint. The second route is shadowed and unreachable.

The Fix

Corrected pattern
Python
from fastapi import FastAPI, APIRouter

router = APIRouter()

# Place static routes before dynamic path parameters to prevent shadowing
@router.get("/users/me")
async def get_current_user():
    return {"user_id": "authenticated_user", "source": "static"}

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

app = FastAPI()
app.include_router(router)

Reordering the route registration ensures that the more specific /users/me path is evaluated first. When a request for /users/me arrives, it matches get_current_user directly, preventing it from falling through to the broader /users/{user_id} pattern.

How This Fails in Real Systems

A financial transaction app registered dynamic currency routing before the static total balance endpoint. For three weeks, attempts to call /transactions/balance returned validation errors because the application parsed the string 'balance' as a transaction UUID, causing severe logging noise and blocking client dashboard rendering.

Key Takeaway

Always register static and literal path endpoints before dynamic path parameters within your routing definition files.
Common mistake: Developers assume FastAPI's router prioritizes more specific (static) paths over less specific (dynamic) ones automatically, leading to unexpected route matching.