Course topics

By WebNest Studio

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

Python
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())
Output
['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

Python
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)
Output
{'id': 2, 'title': 'Deep Python'}
404
['open session', 'close session', 'open session', 'close session']

Splitting an application with APIRouter

Python
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
Output
['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.