FastAPI’s DI is a quiet superpower. Used well, it scales to dozens of endpoints without becoming the boilerplate that other Python frameworks become. This post is the patterns from real codebases.

Typed dep aliases

from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db() -> AsyncIterator[AsyncSession]:
    async with SessionLocal() as session:
        yield session

DBSession = Annotated[AsyncSession, Depends(get_db)]


# In handlers:
@app.get("/users/{id}")
async def get_user(id: int, db: DBSession) -> User:
    ...

DBSession is a type alias. Every endpoint uses db: DBSession instead of db: AsyncSession = Depends(get_db). Cleaner.

For SQLAlchemy 2.0 patterns .

Authenticated user

async def current_user(
    request: Request,
    db: DBSession,
) -> User:
    token = request.headers.get("authorization", "").removeprefix("Bearer ")
    user = await verify_token(db, token)
    if not user:
        raise HTTPException(401, "unauthenticated")
    return user

CurrentUser = Annotated[User, Depends(current_user)]


@app.get("/me")
async def me(user: CurrentUser) -> User:
    return user

Standard pattern. Failure raises HTTPException; user is always set.

For Authentication .

Permissions / role checks

class RequiresRole:
    def __init__(self, role: str):
        self.role = role
    
    async def __call__(self, user: CurrentUser):
        if self.role not in user.roles:
            raise HTTPException(403, f"requires {self.role}")
        return user

AdminUser = Annotated[User, Depends(RequiresRole("admin"))]


@app.delete("/users/{id}")
async def delete_user(id: int, admin: AdminUser, db: DBSession):
    ...

Class-based dep takes init args. The instance is callable. FastAPI calls it as a dep. Clean composition.

Request-scoped vs app-scoped

# App-scoped: created once at startup
@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.http = httpx.AsyncClient()
    app.state.cache = redis.Redis(...)
    yield
    await app.state.http.aclose()
    app.state.cache.close()

# Request-scoped: created per-request via Depends
async def get_db():
    async with SessionLocal() as s:
        yield s

App-scoped: shared resources (HTTP client, cache, ML model). Created once. Request-scoped: per-request state (DB session, current user). Cleaned up at end.

Sub-dependencies

Deps depend on other deps:

async def get_org(user: CurrentUser, db: DBSession) -> Organization:
    return await db.get(Organization, user.org_id)

CurrentOrg = Annotated[Organization, Depends(get_org)]


@app.get("/team")
async def team(org: CurrentOrg, db: DBSession):
    ...

get_org depends on current_user and get_db. FastAPI resolves the graph; calls each in order; caches per request.

Caching within a request

async def expensive_computation(user: CurrentUser):
    return await some_expensive_call(user.id)


@app.get("/a")
async def a(result: Annotated[Result, Depends(expensive_computation)]):
    ...

@app.get("/b")
async def b(
    result1: Annotated[Result, Depends(expensive_computation)],
    result2: Annotated[Result, Depends(expensive_computation)],  # same call
):
    # result1 == result2; expensive_computation called once
    ...

Within a single request, the same dep is called once. Across requests, called per-request.

For across-request caching, use Redis externally.

Background work

from fastapi import BackgroundTasks

@app.post("/signup")
async def signup(payload: SignUp, bg: BackgroundTasks):
    user = await create_user(payload)
    bg.add_task(send_welcome_email, user.id)
    return user

BackgroundTasks runs after the response returns. Fine for fire-and-forget < 100ms work. For heavier or retry-able work, use proper background jobs .

Testing with overrides

async def get_test_db():
    yield mock_db

app.dependency_overrides[get_db] = get_test_db

In tests, swap any dep for a stub. The override is per-app. See Testing FastAPI Apps .

What I’d ship today

Pattern for new FastAPI app:

  1. Annotated aliases for every common dep.
  2. Lifespan for app-scoped resources.
  3. Request-scoped DB session.
  4. CurrentUser + role-based deps.
  5. Sub-deps for derived resources (org from user).
  6. Override-friendly for tests.

For the broader app skeleton see FastAPI + Pydantic v2 + SQLAlchemy 2.0 .

Read this next

If you want my FastAPI app template with all these wired up, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .