Dependency Injection in FastAPI — A Beginner's Breakdown

Posted on Sat 03 October 2026 in Tutorial

Why This Confused Me at First

The first time I saw Depends() scattered through FastAPI route functions, it looked like unnecessary complexity for something simple. Why not just call a function directly inside the route? It took actually building a few endpoints that needed shared logic — authentication, database connections, settings — before the pattern actually clicked. This article is the explanation I wish I'd had earlier.

What "Dependency Injection" Actually Means

Strip away the intimidating name, and the idea is simple: instead of a function creating or fetching everything it needs inside itself, those things get prepared separately and handed to it from the outside. The function declares what it needs, and something else is responsible for providing it.

# Without dependency injection — the function does everything itself
def get_user_data(user_id: int):
    db = connect_to_database()   # created inside the function
    user = db.query(user_id)
    return user

# With dependency injection — the function just declares what it needs
def get_user_data(user_id: int, db: Database):
    user = db.query(user_id)
    return user

In the second version, get_user_data doesn't care how the database connection was created — it just expects to receive one. Something else is responsible for creating and providing it.

How FastAPI Implements This: Depends()

FastAPI's dependency injection system lets you define a function (a "dependency"), and FastAPI automatically calls it and passes the result into your route:

from fastapi import FastAPI, Depends

app = FastAPI()

def get_greeting():
    return "Hello from a dependency!"

@app.get("/hello")
def hello(greeting: str = Depends(get_greeting)):
    return {"message": greeting}

Here's what's happening:

  1. FastAPI sees greeting: str = Depends(get_greeting) in the route's parameters
  2. Before running hello(), FastAPI calls get_greeting() itself
  3. Whatever get_greeting() returns gets passed in as the greeting argument

You never call get_greeting() yourself — FastAPI handles that automatically, every time a request comes in.

Why Not Just Call the Function Directly?

A reasonable question: why not just write greeting = get_greeting() inside the route instead of using Depends()? For something this simple, it genuinely doesn't matter much. But dependency injection pays off once your app grows, in a few concrete ways:

  • Shared logic across many routes, in one place. If ten different routes need the same database connection logic, authentication check, or settings object, you define it once and reuse it everywhere via Depends(), instead of repeating the same setup code in every route.
  • Easier testing. FastAPI lets you override a dependency during tests — swap a real database connection for a fake one, or a real authentication check for a mock user — without touching your route code at all.
  • Automatic handling of setup and teardown. Dependencies can include cleanup logic (closing a database connection, for example) that FastAPI runs automatically after the request finishes.
  • Clear, explicit requirements. Looking at a route's function signature tells you exactly what it depends on — a database, current user, settings — without digging through the function body.

A Practical Example: Database Sessions

This is one of the most common real-world uses of Depends() — managing a database session per request:

from fastapi import Depends

def get_db():
    db = SessionLocal()  # create a new database session
    try:
        yield db          # hand it to the route
    finally:
        db.close()        # always close it after, even if an error occurred

@app.get("/users/{user_id}")
def get_user(user_id: int, db = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    return user

Notice the yield instead of return. This is a special pattern FastAPI supports: everything before yield runs before the route, and everything after yield runs after the route finishes — guaranteeing the database connection gets closed properly, even if the route raises an error. This cleanup pattern would otherwise need to be manually repeated in every single route that touches the database.

A Practical Example: Authentication

Dependency injection is also the standard way to handle "who is making this request" across many protected routes:

from fastapi import Depends, HTTPException, Header

def get_current_user(authorization: str = Header(...)):
    token = authorization.replace("Bearer ", "")
    user = validate_token(token)   # your actual token validation logic
    if not user:
        raise HTTPException(status_code=401, detail="Invalid or missing token")
    return user

@app.get("/profile")
def get_profile(current_user = Depends(get_current_user)):
    return {"username": current_user.username}

@app.post("/posts")
def create_post(current_user = Depends(get_current_user)):
    return {"message": f"Post created by {current_user.username}"}

Both routes get automatic authentication just by declaring current_user = Depends(get_current_user) — no repeated token-checking code, and if the check fails, FastAPI returns the 401 error before the route's own logic ever runs.

Chaining Dependencies

Dependencies can depend on other dependencies, letting you build layered logic:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def get_current_user(token: str = Header(...), db = Depends(get_db)):
    user = db.query(User).filter(User.token == token).first()
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

@app.get("/dashboard")
def dashboard(current_user = Depends(get_current_user)):
    return {"welcome": current_user.username}

Here, get_current_user itself depends on get_db. FastAPI resolves the whole chain automatically — the database session gets created, used to look up the user, and the resulting user gets passed into the route — all from one Depends() call in the route itself.

Why This Matters for GenAI APIs Specifically

This pattern shows up constantly in LLM-backed APIs. A few realistic examples:

def get_llm_client(settings = Depends(get_settings)):
    return OpenAI(api_key=settings.openai_api_key)

@app.post("/chat")
def chat(request: ChatRequest, client = Depends(get_llm_client)):
    completion = client.chat.completions.create(...)
    return completion

This keeps API key management, client setup, and route logic cleanly separated — and makes it trivial to swap in a mock LLM client during testing, so your tests don't actually call a real (and costly) LLM API every time you run them.

A Simple Mental Model

Think of Depends() as saying: "before you run this function, make sure this other thing is ready first, and hand me the result." The route doesn't need to know how that thing gets created — just that it will be there when needed. That separation is the entire point.

Closing Thought

Dependency injection looks like unnecessary ceremony the first time you see Depends() scattered through simple routes — and for truly small apps, it kind of is. But the moment you have shared logic (database access, authentication, settings, API clients) needed across multiple routes, it becomes the difference between clean, testable, DRY code and the same setup logic copy-pasted everywhere. It's one of those FastAPI patterns that feels like overhead until the app grows past a handful of routes — and then it feels indispensable.