Python Tutorial
Dependency Injection and APIRouter
Many endpoints need the same things: a database session, the current user, pagination settings, an API key check. FastAPI's dependency injection system lets you write that logic once as a function (or class) and ask for it with Depends(). FastAPI calls the dependency, passes the result into your endpoint, handles cleanup, and even documents the parameters the dependency needs.
As an application grows, APIRouter lets you split endpoints into modules — books.py, users.py — with shared prefixes, tags and dependencies. This lesson covers both.
Depends()
A dependency is any callable whose parameters FastAPI can fill (query parameters, headers, other dependencies…). Declare it with Annotated[Type, Depends(func)]; a type alias such as Pagination = Annotated[dict, Depends(pagination)] keeps signatures short. Dependencies can depend on other dependencies, forming a tree; within one request each dependency is called once and its result is cached. Classes work too — Depends(MyClass) calls the constructor.
Dependencies with yield
A dependency that uses yield runs setup code, gives a value to the endpoint, and runs cleanup after the response is built — exactly what a database session or a file handle needs. Put cleanup in finally so it always runs. Dependencies can also raise HTTPException, which is how authentication and permission checks are usually implemented.
APIRouter and Project Structure
Create router = APIRouter(prefix="/books", tags=["books"]) in its own module, decorate endpoints with @router.get(...), and add it to the app with app.include_router(router). Routers and include_router accept dependencies=[Depends(...)] to protect a whole group of endpoints. A common layout is app/main.py, app/routers/, app/schemas.py (Pydantic models), app/models.py (database models), app/dependencies.py and app/config.py.
Examples
Reusable dependencies: functions, classes and sub-dependencies
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient
app = FastAPI()
PRODUCTS = [f"product-{n:02d}" for n in range(1, 26)]
def pagination(page: Annotated[int, Query(ge=1)] = 1, size: Annotated[int, Query(ge=1, le=50)] = 10):
return {"offset": (page - 1) * size, "limit": size}
Pagination = Annotated[dict, Depends(pagination)]
class SortOptions: # a class used as a dependency
def __init__(self, sort: str = "name", desc: bool = False):
self.sort = sort
self.desc = desc
def api_key(x_api_key: Annotated[str | None, Header()] = None): # reads the X-API-Key header
if x_api_key != "demo-key":
raise HTTPException(status_code=401, detail="Invalid or missing API key")
return "partner-42"
def current_partner(partner_id: Annotated[str, Depends(api_key)]): # depends on api_key
return {"id": partner_id, "plan": "gold"}
@app.get("/products")
def list_products(page: Pagination, sorting: Annotated[SortOptions, Depends()]):
items = sorted(PRODUCTS, reverse=sorting.desc)
return items[page["offset"]: page["offset"] + page["limit"]]
@app.get("/partner/report")
def report(partner: Annotated[dict, Depends(current_partner)], page: Pagination):
return {"partner": partner, "page": page}
client = TestClient(app)
print(client.get("/products?page=3&size=4").json())
print(client.get("/products?size=3&desc=true").json())
print(client.get("/partner/report").json())
print(client.get("/partner/report", headers={"X-API-Key": "demo-key"}).json())
['product-09', 'product-10', 'product-11', 'product-12']
['product-25', 'product-24', 'product-23']
{'detail': 'Invalid or missing API key'}
{'partner': {'id': 'partner-42', 'plan': 'gold'}, 'page': {'offset': 0, 'limit': 10}}
A yield dependency with setup and cleanup
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
log = []
class FakeSession:
def __init__(self):
log.append("open session")
def query(self, book_id):
if book_id > 2:
raise LookupError(book_id)
return {"id": book_id, "title": ["Python Basics", "Deep Python"][book_id - 1]}
def close(self):
log.append("close session")
def get_session():
session = FakeSession()
try:
yield session # the endpoint runs here
finally:
session.close() # always runs, even after errors
@app.get("/books/{book_id}")
def get_book(book_id: int, session: Annotated[FakeSession, Depends(get_session)]):
try:
return session.query(book_id)
except LookupError:
raise HTTPException(404, "Book not found")
client = TestClient(app)
print(client.get("/books/2").json())
print(client.get("/books/5").status_code)
print(log)
{'id': 2, 'title': 'Deep Python'}
404
['open session', 'close session', 'open session', 'close session']
Splitting an application with APIRouter
from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
# app/dependencies.py
def require_admin(x_role: Annotated[str, Header()] = "user"):
if x_role != "admin":
raise HTTPException(403, "Admins only")
# app/routers/books.py
books_router = APIRouter(prefix="/books", tags=["books"])
@books_router.get("/")
def list_books():
return ["Python Basics", "Deep Python"]
@books_router.get("/{book_id}")
def get_book(book_id: int):
return {"id": book_id}
# app/routers/admin.py — every route here requires an admin
admin_router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(require_admin)])
@admin_router.get("/stats")
def stats():
return {"books": 2, "users": 10}
# app/main.py
app = FastAPI()
app.include_router(books_router)
app.include_router(admin_router, prefix="/api") # final path: /api/admin/stats
client = TestClient(app)
print(client.get("/books/").json(), client.get("/books/7").json())
print(client.get("/api/admin/stats").status_code)
print(client.get("/api/admin/stats", headers={"X-Role": "admin"}).json())
print(client.get("/admin/stats").status_code) # only mounted under /api
['Python Basics', 'Deep Python'] {'id': 7}
403
{'books': 2, 'users': 10}
404
Common Mistakes
- Creating database sessions or clients inside every endpoint instead of using a dependency.
- Forgetting try/finally in a yield dependency, so cleanup is skipped when the endpoint fails.
- Calling the dependency yourself (Depends(get_db())) instead of passing the function (Depends(get_db)).
- Putting all endpoints in one huge main.py instead of APIRouter modules.
- Repeating auth checks in each endpoint instead of router-level dependencies.
Key Points to Remember
- Depends() injects reusable logic: sessions, current user, pagination, permission checks.
- Dependencies can have their own parameters and sub-dependencies; results are cached per request.
- yield dependencies provide setup and guaranteed cleanup.
- APIRouter groups endpoints with a shared prefix, tags and dependencies.
- Annotated type aliases keep endpoint signatures short and consistent.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.