PythonMastery
intermediate 24 min read · lesson 7 of 12 in Web Frameworks

FastAPI Dependency Injection: Auth, DB Sessions, Routers

1 · The lesson

read

Dependency injection is known for needing a lot of setup code, as in large Java frameworks. FastAPI's version is much simpler: a dependency is a plain Python function. You declare param: Annotated[SomeType, Depends(some_fn)] in your endpoint, and FastAPI calls some_fn() for you, caches the result for the request, and injects it. An auth check, a DB session, settings, a paginator, a feature flag, a current-user lookup: they all use the same mechanism, and each takes about three lines.

Run locally with pip install 'fastapi[standard]' pydantic-settings and fastapi dev main.py. Expected output is shown in comments.

Dependencies are also the place where production code and test code differ. If you override one function in a test, the whole request uses the fake. This makes FastAPI apps easy to test.


1. The Minimum Dependency

A dependency is a function. You name it as a parameter via Depends:

python
from typing import Annotated
from fastapi import Depends, FastAPI

app = FastAPI()

def get_greeting() -> str:
    return "Hello"

@app.get("/")
def home(greeting: Annotated[str, Depends(get_greeting)]):
    return {"message": greeting + ", world"}
# GET / returns {"message":"Hello, world"}

Annotated[str, Depends(get_greeting)] reads as "this parameter is a str, sourced by calling get_greeting". FastAPI reads the signature when the route is defined, works out which functions to call, and runs get_greeting() before each request that needs it. The function can take its own dependencies, including request data. FastAPI resolves those first, and their dependencies before them.

The old form greeting: str = Depends(get_greeting) still works. The Annotated form is the style the docs use today. It works better with type checkers and combines easily with Path, Query and Header.


2. Function vs Class Dependencies

There are two kinds, and FastAPI uses them in the same way.

Function: the default, and what you'll use most of the time:

python
def common_pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/items/")
def list_items(p: Annotated[dict, Depends(common_pagination)]):
    return p
# GET /items/?skip=20&limit=5 returns {"skip":20,"limit":5}
+ setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

The dependency's parameters become query parameters of the route, exactly as if they were declared on the route itself. You can reuse one definition in many endpoints.

Class: when the dependency holds state that you want to reuse:

python
class Paginator:
    def __init__(self, skip: int = 0, limit: int = 10):
        self.skip = skip
        self.limit = max(1, min(limit, 100))

    def slice(self, items: list) -> list:
        return items[self.skip : self.skip + self.limit]

@app.get("/items/")
def list_items(p: Annotated[Paginator, Depends()]):
    all_items = ["a", "b", "c", "d", "e"]
    return p.slice(all_items)
+ setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

Depends() with no argument uses the parameter's type annotation as the callable, so here it means Depends(Paginator). You get a fresh instance per request, and you can call methods on it from the handler. Use a class when the dependency has behaviour beyond producing a value.


3. Sub-Dependencies: Dependencies That Depend on Dependencies

FastAPI resolves the whole dependency graph. A dependency can declare its own Depends(...) parameters, and FastAPI fills them in just like the route's.

python
def query_token(token: str = "") -> str:
    return token

def verify_token(token: Annotated[str, Depends(query_token)]) -> str:
    if token != "secret":
        raise HTTPException(401, "Invalid token")
    return token

@app.get("/protected")
def protected(token: Annotated[str, Depends(verify_token)]):
    return {"token": token}
# GET /protected?token=secret returns {"token":"secret"}
# GET /protected?token=nope   returns 401
+ setup added so this can run · defines app, Annotated, HTTPException, Depends
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
def HTTPException(*_a, **_kw):
    print('-> HTTPException() called')
    return _AutoMock('HTTPException()')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

verify_token depends on query_token, which extracts the value from a query parameter. The route just asks for verify_token. FastAPI builds the chain.

Within one request, a dependency is cached by default. If two dependencies both depend on get_db, both receive the same session. Turn this off with Depends(get_db, use_cache=False) when you really need a new value each time.


4. yield Dependencies: Setup + Teardown

This is the most useful pattern. A dependency that needs cleanup uses yield, like a pytest fixture:

python
def get_db():
    db = SessionLocal()
    try:
        yield db                        # value injected into the route
    finally:
        db.close()                      # runs after the response is sent

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Annotated[Session, Depends(get_db)]):
    return db.get(User, user_id)
+ setup added so this can run · defines SessionLocal, User, app, Annotated, Session, Depends
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def SessionLocal(*_a, **_kw):
    print('-> SessionLocal() called')
    return _AutoMock('SessionLocal()')
User = _AutoMock('User')
app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
Session = _AutoMock('Session')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

Code before yield runs at the start of the request. Code after it runs after the response has been sent (or after an exception). This is the behaviour in FastAPI 0.118 and later. Versions 0.106 to 0.117 ran it just before the response was sent, and since 0.121 you can ask for that with Depends(get_db, scope="function"). The try/finally ensures the session closes even if the handler raises. This is the standard FastAPI database pattern: one session per request, opened when needed, always closed.

The same pattern handles transactions, file handles, temporary directories, distributed locks. For async dependencies, async def + yield works the same way (you'll need async with on the resource if it supports it).


5. Auth as a Dependency

Authentication is a good fit for a dependency. Every protected route needs it, the logic is the same in every route, and you want one place to update when the rules change.

python
import os
from fastapi import Header

def get_current_user(authorization: Annotated[str | None, Header()] = None):
    if authorization is None or not authorization.startswith("Bearer "):
        raise HTTPException(401, "Missing bearer token")
    token = authorization.removeprefix("Bearer ")
    if token != os.environ["API_TOKEN"]:
        raise HTTPException(401, "Invalid token")
    return {"username": "surya", "scopes": ["read", "write"]}

@app.get("/me")
def me(user: Annotated[dict, Depends(get_current_user)]):
    return user

@app.get("/admin")
def admin(user: Annotated[dict, Depends(get_current_user)]):
    if "admin" not in user["scopes"]:
        raise HTTPException(403, "Admin only")
    return {"secret": "..."}
+ setup added so this can run · defines app, Annotated, HTTPException, Depends
import os  # noqa: F401
os.environ.setdefault("API_TOKEN", "example-api-token")

# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
def HTTPException(*_a, **_kw):
    print('-> HTTPException() called')
    return _AutoMock('HTTPException()')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

Both routes have the same dependency. The dependency reads the header, validates the token, and returns a user object. Or it raises 401, and then the route stops there. The handler never runs on an invalid request.

Real apps use OAuth2PasswordBearer, JWT validation or session lookups (see auth-jwt for the full pattern). The structure is the same: a dependency function that returns the user or raises.


6. Global and Per-Router Dependencies

Some dependencies should apply to every route: request logging, a tenant header check, a global rate limiter. Attach them at the app level:

python
app = FastAPI(dependencies=[Depends(verify_tenant_header)])
+ setup added so this can run · defines FastAPI, Depends, verify_tenant_header
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def FastAPI(*_a, **_kw):
    print('-> FastAPI() called')
    return _AutoMock('FastAPI()')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')
verify_tenant_header = _AutoMock('verify_tenant_header')

Every route inherits the dependency, with no per-route declaration. The dependency runs (and can raise) before any handler.

You can do the same at the router level, to apply a dependency to only some routes, for example every route under /admin:

python
admin_router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(require_admin)],
)
+ setup added so this can run · defines APIRouter, Depends, require_admin
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def APIRouter(*_a, **_kw):
    print('-> APIRouter() called')
    return _AutoMock('APIRouter()')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')
require_admin = _AutoMock('require_admin')

Dependencies declared on the route, the router and the app all run: global first, then router, then route. The return values from dependencies=[...] are not injected into the handler signature (they're for side-effects: auth checks, logging). Inject only the ones the handler needs as parameters.


7. Pydantic Settings: Config as a Dependency

If you hardcode the database URL, you will have problems when the app runs in more than one environment. Pydantic Settings reads from environment variables, validates types, and gives you a single typed config object:

bash
pip install pydantic-settings
python
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")
    database_url: str
    api_token: str
    debug: bool = False

@lru_cache
def get_settings() -> Settings:
    return Settings()                   # reads env vars + .env file

@app.get("/info")
def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {"debug": settings.debug}
+ setup added so this can run · defines app, Annotated, Depends
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')
Annotated = _AutoMock('Annotated')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')

Settings() raises at startup if a required env var is missing. @lru_cache makes get_settings return one shared object: every dependency call returns the same instance for the lifetime of the process. Override in tests with app.dependency_overrides[get_settings] = lambda: Settings(database_url="sqlite:///:memory:", api_token="test").

You can combine these. A get_db dependency takes Settings as a sub-dependency to find its DSN:

python
def get_db(settings: Annotated[Settings, Depends(get_settings)]):
    engine = create_engine(settings.database_url)
    db = Session(engine)
    try:
        yield db
    finally:
        db.close()
+ setup added so this can run · defines create_engine, Session, Annotated, Settings, Depends, get_settings
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def create_engine(*_a, **_kw):
    print('-> create_engine() called')
    return _AutoMock('create_engine()')
def Session(*_a, **_kw):
    print('-> Session() called')
    return _AutoMock('Session()')
Annotated = _AutoMock('Annotated')
Settings = _AutoMock('Settings')
def Depends(*_a, **_kw):
    print('-> Depends() called')
    return _AutoMock('Depends()')
get_settings = _AutoMock('get_settings')

8. Routers: FastAPI's "Blueprints"

A single main.py works for a small webhook. A real app has dozens of endpoints in clear areas: users, products, orders, admin. APIRouter is the unit of organisation:

python
# routers/users.py
from fastapi import APIRouter, Depends
from typing import Annotated

router = APIRouter(
    prefix="/users",
    tags=["users"],
    dependencies=[Depends(get_current_user)],
    responses={404: {"description": "Not found"}},
)

@router.get("/{user_id}")
def get_user(user_id: int, db = Depends(get_db)):
    return db.get(User, user_id)
+ setup added so this can run · defines get_db, User, get_current_user
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

get_db = _AutoMock('get_db')
User = _AutoMock('User')
get_current_user = _AutoMock('get_current_user')
python
# main.py
from fastapi import FastAPI
from routers import users, products, orders

app = FastAPI()
app.include_router(users.router)
app.include_router(products.router)
app.include_router(orders.router)

The router declares its own prefix, tags (drives Swagger UI grouping), default dependencies, and shared response schemas. The main app file stays short: it only puts the routers together. This is the FastAPI equivalent of Flask blueprints and Django apps. The idea is the same, with lighter syntax.


9. Project Structure for Medium Apps

A project layout that scales from one endpoint to fifty without rewrites:

python
app/
    main.py                 # FastAPI() instance, include_routers, lifespan
    config.py               # Settings, get_settings
    dependencies/
        __init__.py
        auth.py             # get_current_user, require_admin
        db.py               # get_db, get_engine
    models/                 # SQLAlchemy ORM models
        user.py
        product.py
    schemas/                # Pydantic request/response models
        user.py
        product.py
    services/               # business logic, no framework imports
        user_service.py
        product_service.py
    routers/
        users.py
        products.py
    tests/
        conftest.py
        test_users.py
        test_products.py

Three rules:

  • services/ knows nothing about FastAPI. It is plain Python that takes the inputs it needs and returns plain objects. It is easy to unit-test, and easy to reuse if you ever change frameworks.
  • routers/ only connects things. Read the request, call a service, return the response. No SQL queries, no auth checks (those are dependencies), no business logic.
  • schemas/ is for the data the API sends and receives. models/ is for the data you store. Keep them separate, because they become more different as the app grows.

10. Middleware: Concerns That Apply to Every Request

Middleware wraps every request. It runs before the dependency chain and after the response is built. Use it for things that are truly global and don't fit the dependency model: CORS, gzip, request IDs, latency metrics.

python
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import time

app.add_middleware(GZipMiddleware, minimum_size=1000)

app.add_middleware(                          # the middleware added last is the outermost one
    CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

@app.middleware("http")
async def add_process_time_header(request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
    return response
+ setup added so this can run · defines app
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')

Middleware order matters. Each add_middleware call wraps everything added before it, so the last one added is the outermost. Add the CORS middleware last, as above, so that no other middleware removes its headers. Auth, business-rule checks and per-route resources belong in dependencies, not middleware. Middleware applies to every request, so it can't easily say "this applies to these routes but not those".


11. Testing Overrides: The Main Reason to Use Dependencies

Dependencies help you organise code, and they are also the place where tests differ from production. app.dependency_overrides is a dict. Put your fake in it, and the real dependency is replaced until you remove the fake:

python
# tests/test_users.py
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies.db import get_db

def fake_db():
    return InMemoryDB()                 # returns a fake database that needs no setup

app.dependency_overrides[get_db] = fake_db

client = TestClient(app)
r = client.get("/users/1")
assert r.status_code == 200

app.dependency_overrides.clear()        # Important: remove the override before the next test
+ setup added so this can run · defines InMemoryDB
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def InMemoryDB(*_a, **_kw):
    print('-> InMemoryDB() called')
    return _AutoMock('InMemoryDB()')

Now every route that depends on get_db (directly or transitively) uses the fake. You don't need mocking, monkeypatching of import paths, or changes to the handlers. The same pattern overrides auth (get_current_user → lambda: {"username": "test"}), settings, and any external client.

This is the main reason to write anything that changes between environments (DB, auth, config, queue clients) as a dependency, not as a module-level object. The full testing playbook is in fastapi-testing.


Common Mistakes

1. Module-level DB session / client

python
db = SessionLocal()                     # opened at import time, never closed
client = httpx.AsyncClient()            # never closed on shutdown, and tests can't override it
+ setup added so this can run · defines SessionLocal, httpx
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def SessionLocal(*_a, **_kw):
    print('-> SessionLocal() called')
    return _AutoMock('SessionLocal()')
httpx = _AutoMock('httpx')

No per-request lifecycle, no test override, no lifespan cleanup. Use a yield dependency for per-request resources, or open them in lifespan for shared pools that the dependency then provides.

2. Forgetting to clear app.dependency_overrides

python
def test_a():
    app.dependency_overrides[get_db] = fake_db
    # ... test ...
    # forgot to clear, so test_b sees the fake too
+ setup added so this can run · defines fake_db, get_db, app
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

fake_db = _AutoMock('fake_db')
get_db = _AutoMock('get_db')
app = _AutoMock('app')

Use a pytest fixture with yield to set and clear, or call app.dependency_overrides.clear() in a finally. Otherwise the next test runs against your fake without any warning, which can hide a bug and be hard to debug.

3. Business logic in dependencies

A dependency that fetches the user is fine. A dependency that runs your entire order-checkout flow, just because "it returns the order receipt", is the wrong use. Dependencies produce values or control access. Services do the work. The route connects them.

4. Circular dep imports

routers/users.py imports from dependencies/auth.py, which imports from routers/users.py. That is a circular import. Put dependencies in their own package, and don't import routers from it. Dependencies should depend on config, models, and the standard library, nothing else.

5. Using Depends(fn) for things that aren't dependencies

Custom utility functions that you call inside the handler don't need Depends. Just call them. Depends is for things that:

  • Should be injected per-request rather than imported,
  • Want their own params turned into query/header/body parameters,
  • Need yield teardown,
  • Should be overridable in tests.

If none of those apply, it's a regular function call.


🎯 Your Turn: Bearer Auth Dependency + Test Override

Build a get_current_user dependency that validates a bearer token against an env var. Protect two routes with it. Demonstrate how a test would override the dependency to skip the auth check.

Requirements:

  • get_current_user(authorization: Annotated[str | None, Header()] = None) reads the Authorization header. With the None default, a missing header reaches your code (and gets your 401) instead of FastAPI's automatic 422.
  • If it doesn't start with Bearer or the token doesn't match os.environ["API_TOKEN"], raise HTTPException(401, ..., headers={"WWW-Authenticate": "Bearer"}).
  • Return a dict {"username": "surya"} on success.
  • Two protected routes: GET /me and GET /private.
  • Show the test override that replaces the dependency with a function returning a fake user.
python
# main.py
import os
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, status

app = FastAPI()

# TODO 1: define get_current_user(...) to read the header, check it, return a user dict
# TODO 2: GET /me using Depends(get_current_user)
# TODO 3: GET /private using Depends(get_current_user)
# TODO 4: in a comment block, show the TestClient override pattern
Hint 1: Reading headers authorization: Annotated[str | None, Header()] = None in the dependency signature reads the Authorization header. FastAPI matches the parameter authorization to that header (header names are not case-sensitive), and underscores in a parameter name stand for hyphens in the header name. The default is None, so the dependency itself decides what to do when the header is missing.
Hint 2: Override the dependency, don't monkey-patch the environment For the test pattern, define a no-op replacement def fake_user(): return {"username": "test"}, then app.dependency_overrides[get_current_user] = fake_user. Now every route that depends on get_current_user sees the fake. Reset with app.dependency_overrides.clear() after the test.
Show full solution
python
# main.py
import os
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, status

app = FastAPI()


def get_current_user(
    authorization: Annotated[str | None, Header()] = None,
) -> dict:
    """Validate a Bearer token against API_TOKEN. Raise 401 on failure."""
    if authorization is None or not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing Bearer token",
            headers={"WWW-Authenticate": "Bearer"},
        )
    token = authorization.removeprefix("Bearer ")
    expected = os.environ.get("API_TOKEN")
    if expected is None or token != expected:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return {"username": "surya", "scopes": ["read", "write"]}


@app.get("/me")
def me(user: Annotated[dict, Depends(get_current_user)]):
    return user


@app.get("/private")
def private(user: Annotated[dict, Depends(get_current_user)]):
    return {"secret": "for your eyes only", "user": user["username"]}


# ─── tests/test_auth.py ──────────────────────────────────────────────
# from fastapi.testclient import TestClient
# from main import app, get_current_user
#
# def fake_user():
#     return {"username": "test-user", "scopes": ["read"]}
#
# def test_me_with_override():
#     app.dependency_overrides[get_current_user] = fake_user
#     try:
#         client = TestClient(app)
#         r = client.get("/me")          # no Authorization header, but it still passes
#         assert r.status_code == 200
#         assert r.json()["username"] == "test-user"
#     finally:
#         app.dependency_overrides.clear()
#
# def test_me_unauthorized():
#     client = TestClient(app)            # no override, so the real dependency runs
#     r = client.get("/me")
#     assert r.status_code == 401
#     assert r.headers["WWW-Authenticate"] == "Bearer"
+ setup added so this can run · defines
import os  # noqa: F401
os.environ.setdefault("API_TOKEN", "example-api-token")

Two routes share one dependency. The dependency keeps all the auth rules in one place: the header format, the token comparison, the error response and the WWW-Authenticate header. Adding a third protected route is one more Depends(get_current_user).

The test override replaces the entire auth check with a function that just returns a user dict. No header to send, no env var to set, no monkeypatching of os.environ. Every protected route in the suite gets the fake automatically. You need the try/finally so that the override doesn't stay active in the next test. To reuse it, wrap it in a pytest fixture:

python
@pytest.fixture
def authed_client():
    app.dependency_overrides[get_current_user] = lambda: {"username": "test"}
    yield TestClient(app)
    app.dependency_overrides.clear()
+ setup added so this can run · defines pytest, get_current_user, app, TestClient
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

pytest = _AutoMock('pytest')
get_current_user = _AutoMock('get_current_user')
app = _AutoMock('app')
def TestClient(*_a, **_kw):
    print('-> TestClient() called')
    return _AutoMock('TestClient()')

The next lesson, fastapi-testing, uses this pattern for a full test suite.


What You Learned

  • A dependency is a function injected via Annotated[T, Depends(fn)]. FastAPI resolves the whole graph per request.
  • Class dependencies (Depends(Paginator)) work too. They are useful when the value has behaviour, not just data.
  • Sub-dependencies compose, and FastAPI caches each one per request unless you pass use_cache=False.
  • yield dependencies give you setup + teardown. This is the standard DB-session pattern.
  • Auth as a dependency is the standard FastAPI pattern: header check + token validation + user object, applied to every protected route.
  • App-level and router-level dependencies=[...] apply to every route in scope. Return values from these are not injected into the handler.
  • Pydantic Settings + get_settings gives you a typed config object read from environment variables. Wrap it with @lru_cache so that there is only one instance.
  • APIRouter organises routes by area, with a prefix, tags, shared dependencies and shared response schemas.
  • Project structure: separate services/ (pure logic), routers/ (connecting requests to services), schemas/ (API data), models/ (storage), dependencies/ (the parts tests can replace).
  • Middleware is for things that apply to every request. Auth and per-resource work belong in dependencies.
  • app.dependency_overrides is the most useful testing feature. You can replace any dependency with a fake, without monkeypatching. Always clear it between tests.

Next: FastAPI Testing: TestClient, async tests with httpx.AsyncClient, fixtures for isolated state, dependency overrides done properly, and good testing habits for a FastAPI service in CI.

Found this useful? Share it with a friend who’s learning Python.