Learn / Python and FastAPI Backend Engineering
Advanced Dependency Injection
Design FastAPI dependency boundaries for request-scoped sessions, authenticated users, framework-free services, and test overrides that cannot leak between cases.
Course: Python and FastAPI Backend Engineering. Level: Intermediate. Topic: Backend craft.
Stage: basic - Basic dependency boundaries - Dependency injection as an API boundary. Use FastAPI dependencies to expose request-scoped resources, auth, settings, and test seams without global state.
Outcomes
- Thread request-scoped session and user providers through routes without hidden globals.
- Build dependency overrides for auth and repositories that are isolated per test.
- Keep business services framework-free while still using FastAPI dependencies at the HTTP edge.
- Split routes by domain with APIRouter.
- Use dependency injection for settings, auth, and database access.
- Keep route functions thin by moving business logic into services.
- Use lifespan for startup and shutdown resources.
Concepts
- request-scoped provider
- dependency override
- auth boundary
- framework-free service
- FastAPI Architecture
- Routers organize HTTP surface area
- Dependencies make boundaries explicit
- Python + FastAPI foundations
- Guided practice
Concept flow
Show how request providers feed a framework-free service and where tests can replace auth or persistence safely.
- Request
- Auth provider
- Session provider
- Repository provider
- Service function
- Override test
Session flow
- Separate provider boundaries from service work (concept, 11 min) — Use dependency functions to translate request state into plain service inputs.
- Providers own framework details like Depends, tokens, sessions, and app settings.
- Service functions should accept plain objects, protocols, and values.
- Build request-scoped auth and persistence (walkthrough, 23 min) — Compose current_user, get_session, and get_order_repository into a route that exposes every dependency clearly.
- Async sessions belong to the request and should be yielded by a provider that owns cleanup.
- A route signature is a reviewable dependency graph when providers are named well.
- Prove override isolation (exercise, 14 min) — Write an override-based route test and prove overrides are cleared after the case.
- Override tests should exercise real routing, validation, and response behavior.
- Fixture cleanup is part of the dependency contract, not test hygiene trivia.
- Routers, Dependencies, Services, and Lifespan: Routers organize HTTP surface area (concept, 75 min) — APIRouter lets each domain own its paths, tags, dependencies, and response models. The main app wires routers together and stays small.
- Retained source example: Router module
from fastapi import APIRouter
router = APIRouter(prefix='/tasks', tags=['Tasks'])
@router.get('')
def list_tasks() -> list[dict[str, str]]:
return [{'title': 'write tests'}]
- Routers, Dependencies, Services, and Lifespan: Dependencies make boundaries explicit (walkthrough, 75 min) — Dependencies can provide settings, authenticated users, database sessions, or service objects. Tests can override dependencies to isolate behavior without changing application code.
- Retained source example: Service dependency
from typing import Annotated
from fastapi import Depends
class Clock:
def now(self) -> str:
return '2026-01-01T00:00:00Z'
def get_clock() -> Clock:
return Clock()
ClockDep = Annotated[Clock, Depends(get_clock)]
- Routers, Dependencies, Services, and Lifespan: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use routers for domain boundaries and app construction for wiring.
- Practice: Keep business logic in services that can be tested without HTTP.
- Practice: Use lifespan for resources that need startup and shutdown.
- Avoid: Putting every route in main.py until the file becomes unreviewable.
- Avoid: Creating hidden globals instead of explicit dependencies.
- Avoid: Performing long startup work without failure reporting.
- Routers, Dependencies, Services, and Lifespan: references (review, 2 min) — Original references retained from the legacy library.
- FastAPI documentation: https://fastapi.tiangolo.com/
- Pydantic documentation: https://docs.pydantic.dev/
- FastAPI bigger applications: https://fastapi.tiangolo.com/tutorial/bigger-applications/
- FastAPI dependencies: https://fastapi.tiangolo.com/tutorial/dependencies/
Code example
Python in app/dependencies.py.
from collections.abc import AsyncIterator
from typing import Annotated, Protocol
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
router = APIRouter(prefix="/orders", tags=["orders"])
class User:
def __init__(self, id: UUID, email: str) -> None:
self.id = id
self.email = email
class OrderRepository(Protocol):
async def list_for_user(self, user_id: UUID) -> list[str]: ...
class SqlOrderRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def list_for_user(self, user_id: UUID) -> list[str]:
rows = await self.session.execute(
text("""
select order_number
from orders
where user_id = :user_id
order by created_at desc
"""),
{"user_id": str(user_id)},
)
return [str(row.order_number) for row in rows]
async_session = async_sessionmaker(expire_on_commit=False)
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session() as session:
yield session
async def current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
user = await users.find_by_token(token)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
async def get_order_repository(
session: Annotated[AsyncSession, Depends(get_session)],
) -> OrderRepository:
return SqlOrderRepository(session)
async def list_visible_orders(user: User, orders: OrderRepository) -> list[str]:
return await orders.list_for_user(user.id)
@router.get("", response_model=list[str])
async def list_orders(
user: Annotated[User, Depends(current_user)],
orders: Annotated[OrderRepository, Depends(get_order_repository)],
) -> list[str]:
return await list_visible_orders(user, orders)
Walkthrough examples
- Override pack for authenticated order listing — A team needs route-level confidence without calling real auth, real databases, or leaking fake providers between tests.
- File: app/dependencies.py
- File: app/orders/routes.py
- File: app/orders/service.py
- File: tests/test_order_dependencies.py
- Extract current_user, get_session, and get_order_repository as named providers with one lifetime each.
- Move list_visible_orders into a plain service function that accepts User and OrderRepository.
- In a route test, override current_user and get_order_repository with fake providers.
- Clear app.dependency_overrides in fixture teardown and add a test proving unauthorized behavior returns after cleanup.
- Router module — Additional example retained from the legacy lesson.
- Retained source code:
from fastapi import APIRouter
router = APIRouter(prefix='/tasks', tags=['Tasks'])
@router.get('')
def list_tasks() -> list[dict[str, str]]:
return [{'title': 'write tests'}]
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Service dependency — Additional example retained from the legacy lesson.
- Retained source code:
from typing import Annotated
from fastapi import Depends
class Clock:
def now(self) -> str:
return '2026-01-01T00:00:00Z'
def get_clock() -> Clock:
return Clock()
ClockDep = Annotated[Clock, Depends(get_clock)]
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Lifespan resource setup — Additional example retained from the legacy lesson.
- Retained source code:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.ready = True
yield
app.state.ready = False
app = FastAPI(lifespan=lifespan)
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Name every provider in the route signature and write down its lifetime: request, app, or test-only.
- Override current_user and get_order_repository in a route test, then clear overrides in teardown.
- Move the business decision into a framework-free service function and call it from the route.
- Identify a thin route: Which route style is easier to test: one that calls a service function, or one with all business rules inline?
- Hint: Think about unit testing without HTTP.
- Reference solution: one that calls a service function
- Accepted answers: one that calls a service function | service function | thin route
Checklist
- Route functions depend on providers, not module-level sessions or users.
- Application services accept plain objects or protocols instead of Depends.
- Tests override auth and persistence providers without touching production code.
- Overrides are reset after each test so one case cannot change another.
- Override keys use the original provider callable, not a wrapper or duplicate import.
Quiz prompts
- Where should Depends usually appear in a layered FastAPI service? — FastAPI dependencies are excellent at the edge, but core services stay easier to test and reuse when they accept plain parameters.
- A test overrides current_user but never clears app.dependency_overrides. What is the risk? — Overrides are app-level state. Treat them like test fixtures that must be scoped and cleaned up.
- Why model repositories as protocols or small interfaces in this lesson? — A small repository protocol keeps the service framework-free and makes override tests focused on route behavior.
- Which dependency should normally be request-scoped? — Database sessions and authenticated users are tied to one request; settings and pure helpers usually have different lifetimes.
- A dependency override silently stops working after a refactor. What should you inspect first? — FastAPI overrides are keyed by the dependency callable object. Wrapping or importing a different provider can miss the intended seam.
- Why are FastAPI dependency overrides useful in tests? — Overrides let tests supply fake settings, users, clients, or repositories.
Flashcards
- Where should FastAPI Depends live? Keep Depends at the HTTP edge: route signatures and provider functions. Services should accept plain objects or protocols.
- Why yield an AsyncSession from a provider? The provider owns request-scoped setup and cleanup, so each request gets a bounded session lifecycle.
- What must every dependency override fixture do? Install fakes only for the test scope and clear app.dependency_overrides afterward.
- Why depend on an OrderRepository protocol? It names the behavior the service needs while letting production use SQL and tests use a fake.
- What is the auth provider boundary? It turns token/request details into a User object or raises a client-safe auth error before service code runs.
- How is current_user different from require_admin? current_user authenticates identity; require_admin or require_scope authorizes what that identity may do.
Labs
- Build isolated auth and repository overrides — Refactor one authenticated route so tests can replace auth and persistence without changing production route code.
- Create current_user, get_session, and get_order_repository providers with documented lifetimes.
- Move route business logic into a framework-free service that accepts a User and repository port.
- Write one successful route test using fake auth and fake repository overrides.
- Write one cleanup proof: after override teardown, the same route requires real auth again.
- Add a require_scope provider and override only current_user to prove authorization still runs.
- Tests prove a fake user can read only the fake repository data assigned to that user.
- Override cleanup is verified by a failing/unauthorized request after teardown.
- Review notes include the provider graph and the command output from the override tests.
Challenge
- Review-ready FastAPI dependency graph (Stretch) — Turn a route with hidden globals into an explicit dependency graph: request-scoped session, authenticated user, repository provider, framework-free service, and isolated override tests.
- The route signature makes auth and persistence dependencies visible.
- The service function can be called without importing FastAPI.
- Override tests cover success, auth failure, fake repository behavior, and cleanup.
- The review note names provider lifetimes and one risk if an override leaks.
Canonical lesson URL