Updated Jun 22, 2026

Dependency Injection with Depends()

By now your Book service can take a validated request body, shape a clean response, and return honest status codes. But look closely at your endpoints and you'll start to notice the same chores repeating: every list endpoint re-reads skip and limit, every protected endpoint re-checks the same token, every database-touching endpoint will (in two phases) open and close a session the same way. Copy-paste that logic into ten endpoints and you've signed up to fix the same bug ten times.

FastAPI has a built-in answer, and it's one of the framework's best ideas. Before any code, let's get the mental model — because the syntax is trivial once the shape clicks.

The idea: declare what you need, the framework provides it

📝 Dependency injection in FastAPI means: a path operation declares what it needs as a parameter, and FastAPI provides it by calling a function you wrote — a dependency. You don't fetch the thing yourself. You announce "I need a current user / a database session / pagination settings," and the framework runs the right function and hands you the result, already prepared.

If that "the framework calls your function for you" gesture feels familiar, it should — it's inversion of control, the exact idea behind the word "framework" (What a Framework Even Is). You're not calling the dependency; you register what you want, and FastAPI calls it on your behalf at request time. Don't call us, we'll call you.

💡 The lovely part: a FastAPI dependency is just a plain function. No special base class, no DI container to configure, no XML, no registration step. If you can write a function, you can write a dependency. That's the whole system.

A simple dependency

Let's start concrete. Pagination — skip and limit — is the textbook case, because every list endpoint in the Book service wants the same two query parameters with the same defaults and the same sanity checks.

Here's the dependency itself. It's pure Python — no FastAPI imports, no running server needed — so you can run it right here:

def pagination_params(skip: int = 0, limit: int = 10) -> dict:
    # one place to keep pagination sane for the whole API
    safe_limit = min(limit, 100)   # never let a caller ask for 10,000 rows
    return {"skip": max(skip, 0), "limit": safe_limit}

# simulate FastAPI calling it with values pulled from the query string
print(pagination_params())                 # the defaults
print(pagination_params(skip=20, limit=5)) # a normal request
print(pagination_params(skip=-3, limit=999))  # a hostile request, clamped

What just happened: pagination_params is an ordinary function with two parameters that have defaults. It clamps nonsense — negative skip, a limit of 999 — into something safe and returns a tidy dict. Nothing here is FastAPI-specific yet; it's testable and runnable in isolation, which is exactly the point.

Now wire it into an endpoint. This part needs the running app, so it's shown as plain code:

from fastapi import FastAPI, Depends

app = FastAPI()

# pretend store; real DB arrives in Phase 7
BOOKS = [{"id": i, "title": f"Book {i}"} for i in range(1, 51)]

def pagination_params(skip: int = 0, limit: int = 10) -> dict:
    return {"skip": max(skip, 0), "limit": min(limit, 100)}

@app.get("/books")
def list_books(params: dict = Depends(pagination_params)):
    start = params["skip"]
    end = start + params["limit"]
    return BOOKS[start:end]

What just happened: params: dict = Depends(pagination_params) is the whole trick. You did not call pagination_params() yourself — you handed the function to Depends(). When a request hits /books, FastAPI calls it for you and injects the returned dict as params. Your endpoint body never touches skip or limit directly; it just receives a ready-made, already-clamped dict.

💡 Here's the part people miss the first time: the dependency's own parameters become part of the endpoint's public interface. Because pagination_params declares skip and limit, a request to /books?skip=20&limit=5 works — FastAPI reads those query params, validates them as int (the same type-hint-driven validation from earlier phases), passes them in, and they even show up in the automatic /docs. The dependency contributed query parameters to an endpoint that never mentions them.

Why DI here is genuinely powerful

That pagination example is small, but the payoff scales hard. Three wins, all from the same mechanism:

  • Write shared logic once. Add params: dict = Depends(pagination_params) to /books, /authors, /reviews — every list endpoint gets identical, sane pagination. Change the max limit in one function and the whole API updates. No copy-paste, no drift.
  • Testable by swapping. Because the dependency is just a function, your tests can replace it with a fake (FastAPI has a dependency_overrides hook for exactly this). Need a test to run as an admin user? Override the auth dependency to return one. No real tokens, no real database. We'll lean on this in the testing phase.
  • Self-documenting. A dependency's parameters flow into the interactive docs automatically, so the contract stays honest without you maintaining it by hand.

And dependencies compose. 📝 A dependency can itself depend on another via Depends(...) — these are sub-dependencies, and FastAPI resolves the whole chain for you, in order, before your endpoint runs:

from fastapi import Depends

def db_connection():
    return {"conn": "fake-connection"}

# this dependency needs the one above — a sub-dependency
def book_repository(db: dict = Depends(db_connection)):
    return {"repo": "books", "using": db["conn"]}

@app.get("/books/{book_id}")
def get_book(book_id: int, repo: dict = Depends(book_repository)):
    return {"book_id": book_id, "served_by": repo}

What just happened: get_book asks only for book_repository. But book_repository itself asks for db_connection. FastAPI walks the chain: it calls db_connection first, feeds the result into book_repository, then hands that result to your endpoint. You declared one need at the top and the framework assembled the whole stack underneath. That's how a real app layers DB → repository → endpoint without any glue code.

yield dependencies: setup before, teardown after

Some things you depend on need to be opened and then reliably closed — a database session, a file, a network client. You want code to run before the request handler and more code to run after it, even if the handler blew up. FastAPI's answer is a dependency that uses yield instead of return.

📝 A yield dependency runs everything up to the yield as setup, hands the yielded value to your endpoint, and runs everything after the yield as teardown once the response is sent. If you've met context managers in Python (Python From Zero covers the with statement), this is the same setup/teardown shape — the code after yield is your finally.

The canonical use is a database session. The real version lands in Phase 7; here's the shape so the pattern is in your hands now:

from fastapi import Depends

# stand-in for a real session; Phase 7 makes this a SQLModel Session
def get_db():
    db = {"session": "open", "queries": []}   # setup: open the session
    print("DB session opened")
    try:
        yield db                              # hand it to the endpoint
    finally:
        print("DB session closed")           # teardown: always runs

@app.get("/books-from-db")
def list_books_from_db(db: dict = Depends(get_db)):
    db["queries"].append("SELECT * FROM books")
    return {"books": [], "ran": db["queries"]}

What just happened: When a request arrives, FastAPI runs get_db up to yield, opening the "session" and injecting it as db. Your endpoint uses it. After the response is sent, FastAPI resumes the function past yield and runs the teardown — closing the session. One function owns the entire lifecycle of the resource, so an endpoint can never forget to clean up.

⚠️ The teardown runs even if your endpoint raises an exception. That's the entire reason for the try/finally: a request that errors out still closes its database session, so you don't leak connections every time something goes wrong. This is the single most important reason DB sessions are done as yield dependencies and not opened ad hoc inside handlers.

Auth as a dependency (preview) + where you can attach it

The same mechanism is how authentication works in FastAPI — and it's a perfect fit. "This endpoint requires a logged-in user" is exactly "this endpoint depends on there being a current user." Full auth — real tokens, OAuth2, JWT — is Phase 8; here's the shape so you see the seam:

from fastapi import Depends, Header, HTTPException

def get_current_user(x_token: str = Header(default="")):
    if x_token != "secret-token":
        # no valid credentials → stop here, the endpoint never runs
        raise HTTPException(status_code=401, detail="Not authenticated")
    return {"username": "ada", "role": "reader"}

@app.post("/books")
def create_book(title: str, user: dict = Depends(get_current_user)):
    return {"created_by": user["username"], "title": title}

What just happened: create_book depends on get_current_user. FastAPI runs that dependency first; if the token is missing or wrong it raises 401 and your endpoint body never executes. If it passes, the endpoint receives the authenticated user. Notice the same pattern as pagination — declare the need, the framework satisfies (or rejects) it before you run. (Notice too that get_current_user could itself Depends on get_db to look the user up — sub-dependencies again.)

And you don't have to attach a dependency endpoint-by-endpoint. There are three reuse levels:

  • Path level — in the endpoint's parameters, as above. Affects that one route.
  • Router levelAPIRouter(dependencies=[Depends(get_current_user)]) applies it to every route on that router. Great for "everything under /admin requires auth."
  • App levelFastAPI(dependencies=[Depends(...)]) applies it to every endpoint, ideal for cross-cutting concerns like a global API-key check.

(You'll meet routers properly in Phase 9; the takeaway now is that the same Depends() scales from one route to the whole app.)

💡 Step back and see the throughline: Depends() is the backbone you'll lean on for the rest of this guide. Database sessions in Phase 7 and authentication in Phase 8 are both just dependencies. Learn this one mechanism well and those phases become "apply the thing you already know" rather than new machinery.

Recap

  1. Dependency injection = an endpoint declares what it needs as a parameter (Depends(func)), and FastAPI calls your function and injects the result. It's inversion of control — the framework calls your code, not the other way around.
  2. A dependency is a plain function. No container, no base class. Its own parameters become part of the endpoint's interface and show up in the automatic docs.
  3. DI lets you write shared logic once (pagination, auth, DB), makes endpoints testable by swapping dependencies, and keeps the API self-documenting. Dependencies can depend on other dependencies (sub-dependencies), resolved as a chain.
  4. A yield dependency runs setup before the request and teardown after — the standard pattern for opening and closing resources like DB sessions, mirroring Python's with.
  5. The teardown of a yield dependency runs even when the endpoint raises, which is exactly why DB sessions use it — errored requests still clean up.
  6. Auth fits naturally as a dependency, and dependencies attach at the path, router, or app level. Depends() is the backbone for databases and auth in the phases ahead.

Quick check

Lock in the core idea before moving on:

[
  {
    "q": "When you write `params: dict = Depends(pagination_params)`, who calls `pagination_params`?",
    "choices": ["You call it yourself before the endpoint runs", "FastAPI calls it and injects the result", "It is never called; Depends just documents it", "Pydantic calls it during model validation"],
    "answer": 1,
    "explain": "That's inversion of control: you declare the dependency and FastAPI calls the function for you at request time, passing its return value into your endpoint."
  },
  {
    "q": "Why is a database session typically written as a `yield` dependency with try/finally?",
    "choices": ["yield makes the query run faster", "So the session is shared globally across all requests", "So teardown (closing the session) runs after the response, even if the endpoint raised", "Because FastAPI cannot inject objects created with return"],
    "answer": 2,
    "explain": "Code after `yield` runs as teardown once the response is sent, and the `finally` guarantees it runs even on an exception — so errored requests still close their session and don't leak connections."
  },
  {
    "q": "You want every route under an admin router to require authentication. What's the cleanest place to attach `Depends(get_current_user)`?",
    "choices": ["On each endpoint individually, repeated everywhere", "At the router level, e.g. APIRouter(dependencies=[Depends(get_current_user)])", "Inside the Pydantic response model", "It can only ever be attached per-path"],
    "answer": 1,
    "explain": "Dependencies attach at the path, router, or app level. Putting it on the router applies it to every route on that router in one place — no per-endpoint repetition."
  }
]

← Phase 4: Response Models & Status Codes · Guide overview · Phase 6: Async & Concurrency →

Check your understanding

1. When you write `params: dict = Depends(pagination_params)`, who calls `pagination_params`?

2. Why is a database session typically written as a `yield` dependency with try/finally?

3. You want every route under an admin router to require authentication. What's the cleanest place to attach `Depends(get_current_user)`?

Was this page helpful?