If you have used Express, you know middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.: functions that sit between the request and your handler, adding data or blocking access. FastAPI takes a fundamentally different approach. Instead of a chain of middlewares that all run on every request, FastAPI uses dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. injection, you declare what each endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. needs, and the framework figures out how to provide it. This distinction matters because it means your endpoints only run the code they actually require, and the wiring is explicit rather than implicit.
When AI generates FastAPI code, it almost always uses Depends(). Understanding how the system resolves dependencies is essential for reading and reviewing that code.
How Depends() works
At its core, Depends() wraps a callable. When FastAPI sees a parameter annotated with Depends(some_function), it calls some_function first, then passes the result to your endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users..
from fastapi import Depends, FastAPI
app = FastAPI()
def get_settings():
return {"app_name": "My App", "debug": True}
@app.get("/info")
def read_info(settings: dict = Depends(get_settings)):
return {"app": settings["app_name"]}When a request hits /info, FastAPI does not just call read_info. It first calls get_settings(), takes the returned dictionary, and injects it as the settings parameter. Your endpoint never worries about how settings are loaded, it just declares that it needs them.
This is the core mental model: the endpoint declares what it needs, the dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. provides it.
DependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. resolution order
When an endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. has multiple dependencies, FastAPI resolves them in the order they appear in the function signature. This matters when one dependency might affect another.
def get_db():
print("1. Creating DB session")
return "db_session"
def get_current_user():
print("2. Fetching current user")
return {"id": 1, "name": "Alice"}
@app.get("/dashboard")
def dashboard(
db = Depends(get_db),
user = Depends(get_current_user)
):
return {"user": user["name"]}Output on each request:
1. Creating DB session
2. Fetching current userThe order is predictable, left to right (or top to bottom in the parameter list). This predictability is a feature: you can reason about what runs when just by reading the function signature.
Nested dependencies
Dependencies can depend on other dependencies. This is where the system gets powerful, and where reading AI-generated code gets tricky.
def get_db():
print("A. get_db")
return "db_session"
def get_settings():
print("B. get_settings")
return {"secret": "abc123"}
def get_current_user(
db = Depends(get_db),
settings = Depends(get_settings)
):
print("C. get_current_user")
return {"id": 1, "name": "Alice"}
@app.get("/profile")
def profile(user = Depends(get_current_user)):
return userThe resolution order is bottom-up. FastAPI sees that profile needs get_current_user, which needs get_db and get_settings. So the actual execution order is:
A. get_db
B. get_settings
C. get_current_userThink of it like a tree that resolves from the leaves to the root. The endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. is the root, and FastAPI walks to the leaves first, resolving dependencies before the things that depend on them.
Depends() parameter, always click through to its definition and check if it has its own dependencies. The actual execution path might be much longer than it looks from the endpoint signature.Sub-dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. caching
Here is a scenario that trips up many developers. What if two dependencies both depend on the same sub-dependency?
def get_db():
print("Creating DB session")
return "db_session"
def get_user(db = Depends(get_db)):
return {"id": 1}
def get_permissions(db = Depends(get_db)):
return ["read", "write"]
@app.get("/check")
def check(
user = Depends(get_user),
perms = Depends(get_permissions)
):
return {"user": user, "permissions": perms}How many times does get_db run? Once. FastAPI caches sub-dependency results within a single request by default (use_cache=True). Both get_user and get_permissions receive the same db_session object.
This is critical for database sessions, you want every part of a request to use the same sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. so that transactions work correctly.
You can disable caching for dependencies that should run fresh every time:
def get_random_id():
import random
return random.randint(1, 1000)
@app.get("/test")
def test(
id1 = Depends(get_random_id), # cached: same value
id2 = Depends(get_random_id, use_cache=False) # fresh call: different value
):
return {"id1": id1, "id2": id2}| Caching behavior | When to use |
|---|---|
use_cache=True (default) | Database sessions, settings, user objects, anything expensive or stateful |
use_cache=False | Random values, timestamps, request-specific IDs |
Class-based dependencies
Dependencies do not have to be functions. Any callable works, including classes. When FastAPI calls a class, it instantiates it, and the instance becomes the injected value.
class Pagination:
def __init__(self, skip: int = 0, limit: int = 10):
self.skip = skip
self.limit = limit
@app.get("/items")
def list_items(pagination: Pagination = Depends(Pagination)):
return {
"skip": pagination.skip,
"limit": pagination.limit
}FastAPI extracts skip and limit from the query stringWhat is query string?The part of a URL after the ? that carries optional key-value pairs (like ?page=2&limit=10) used for filtering, sorting, or pagination. and passes them to Pagination.__init__. The endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. receives a Pagination instance with both values set.
Depends() argument, you can use a shorthand: pagination: Pagination = Depends(). FastAPI infers the callable from the type. AI generators use both forms, they are equivalent.Reading AI-generated dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. chains
When AI generates a FastAPI app, the dependency graph is usually scattered across multiple files. Here is a systematic approach to reading it:
- Start at the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.. Identify every
Depends()in the parameter list. - Follow each dependency. Open its definition and check if it has its own
Depends()parameters. - Draw the tree. For anything beyond two levels, sketch it:
endpoint -> dep A -> dep B -> dep C. - Check for shared sub-dependencies. If the same function appears multiple times, remember it is cached, it runs once.
- Look for side effects. Dependencies that modify global state, write to databases, or call external APIs are the ones most likely to cause bugs.
# Example dependency tree for a typical AI-generated endpoint
POST /orders
├── get_db() ← creates database session
├── get_current_user()
│ ├── get_db() ← cached, same session as above
│ └── get_token() ← extracts token from headers
└── get_rate_limiter()
└── get_db() ← cached again, still same sessionReading this tree tells you that the endpoint runs 4 unique dependency functions despite get_db appearing 3 times. The sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. is shared across all of them. If any dependency raises an exception, the endpoint never executes.