Dependency Injection

Depends: declaring what an endpoint needs and letting the framework supply it - the feature that most distinguishes FastAPI.

Overview

The whole idea

Dependency injection means that a function declares what it needs, and something else supplies it. In FastAPI, you write a small function that produces a value, such as a database session or the current user. You then list that function as a parameter of your endpoint. The framework calls it first and passes in the result.

This solves a common problem. Many endpoints need the same things, and writing the setup code in every handler is tedious and easy to get wrong. With dependencies, the setup lives in one place. Your handler stays short and focuses on its own job.

This article begins with the whole idea in its simplest form, using the Depends function. It then shows that a dependency can see the request, so it can read headers, query values, and cookies just as a handler can. A dependency can also raise an error, which stops the request before your handler ever runs. That is how authentication usually works.

The next section explains caching within a request. If two parts of the same request ask for the same dependency, FastAPI runs it once and shares the result. That detail affects database sessions in particular.

The article ends by looking at what dependency injection replaces. Global variables, repeated setup code, and hand-written decorators all become unnecessary. Once the idea clicks, you will use it in nearly every endpoint.

The term sounds heavy, and in some languages it is. In FastAPI it is light. A dependency is only a function, and the injection is only a parameter. There is no container to configure and no registry to maintain. That simplicity is why the feature gets used everywhere, from authentication to database access to shared query helpers. Each section is short and ends with a habit you can apply straight away, so you finish with working knowledge and a feature you will reach for daily.

def pagination(limit: int = 10, offset: int = 0):
    return {"limit": min(limit, 100), "offset": offset}

@app.get("/modules")
def list_modules(page: dict = Depends(pagination)):
    ...

Depends(fn) means: call fn, and pass what it returns.

There's no registry, no holder, no config and no base class. A dependency is a function. Declaring one is a default value in a signature. That simplicity is why the feature is worth learning properly. It's much smaller than "dependency injection" often implies.

Worth knowing

A dependency is an ordinary function. Depends(fn) calls it and passes the result; there is no registry and no container.
Its own parameters are request parameters, so a dependency can read query values, headers and bodies — and they all appear in the schema.
Raising inside a dependency stops the request before the handler runs, which is why authentication belongs there.
Results are cached per request: two parameters depending on the same function produce one call.
Depends(fn, use_cache=False) forces a fresh call each time it is referenced.
The value is not the wiring — it is that the requirement is in the signature, where the documentation and the reader can both see it.

Dependency Injection

Declaring what an endpoint needs and letting the framework supply it.

A dependency is just a function

Declare a parameter as Depends(fn) and FastAPI calls fn and passes the result. Nothing is registered anywhere.

example_01.pyFastAPI
Output

Its parameters are the request, too

A dependency reads query parameters, headers and bodies exactly as a handler does — and they appear in the documentation.

example_02.pyFastAPI
Output

Raising from a dependency

A dependency that raises stops the request before the handler runs. That is what makes it the right place for authentication.

example_03.pyFastAPI
Output

Resolved once per request

Two parameters depending on the same function get one call, not two. The result is cached for the life of the request.

example_04.pyFastAPI
Output

Turning the cache off

use_cache=False forces a fresh call — for anything that should differ per use, such as a generated identifier.

example_05.pyFastAPI
Output

What it replaces

The same endpoint written both ways. The dependency version has the requirement in the signature, where the documentation can see it.

example_06.pyFastAPI
Output

Compare the two versions in the last editor above. Both work. Both even doc the header, because both declare it.

The gap is where the need lives. In the hand-written version it's four lines at the top of a handler, repeated in each handler that needs it. Diverging quietly as they're edited. In the dependency version it's one function. Each endpoint declares a param.

The scaling gap is the point. Ten endpoints needing a user means ten params and one function. Not forty lines that must agree.

Dependencies see the request

The key second fact: a dependency's own params are request params. It can declare query values, headers, cookies, path params and a body, using just the syntax a handler uses.

So pagination above doesn't receive the request and dig through it. It declares limit and offset. FastAPI supplies them from the query string, converted and checked.

That has a result people miss: those params appear in the OpenAPI doc. An endpoint based on search_filters docs q, track and x-locale as its own params. As far as a caller is concerned they're. The abstraction doesn't hide anything from the contract.

Raising

A dependency that raises stops the request. The handler never runs.

That is what makes it the right home for authentication, authorisation and any precondition:

def current_user(authorization: str = Header(default="")):
    if token not in TOKENS:
        raise HTTPException(401, "Sign in first")
    return TOKENS[token]

each endpoint that needs a user now writes one param. There's no possibility of an endpoint losing the check and no possibility of two endpoints checking differently. Which is just the failure mode of doing it by hand.

Caching within a request

If two params depend on the same function, it's called once. The result is cached for the duration of that request and shared.

This matters more than it first appears. Dependencies compose. A handler might depend on current_user. Also on permissions. This Itself depends on current_user. Without caching, the token would be decoded twice. With it, once.

The cache is per request. Nothing is shared between requests. This Is correct. A cached user leaking into the next request would be a serious bug rather than an tuning.

Depends(fn, use_cache=False) opts out, for anything that should truly differ per link. A made identifier, a fresh timestamp, a new random value.

When something is not a dependency

Two habits worth avoiding.

Wrapping a thing trivial. Depends on a function that returns a constant is indirection with no gain. Import the constant.

Putting business logic in one. A dependency should give a thing the handler needs. A user, a session, a checked set of filters. A dependency that performs the step and returns a result has moved the endpoint's work into its signature. Here It's harder to find and harder to test.

The test: could you show it as "this endpoint needs an X"? Then it's a dependency. If it's "this endpoint does Y", it isn't.

Types and the return value

page: dict = Depends(pagination) annotates the param. That annotation is docs for a reader rather than a thing enforced. The value is what the dependency returned.

For anything real, return a model rather than a dict. current_user returning a User gives each handler field access and editor completion. Makes the dependency's contract clear.

There's a newer spelling using Annotated that is worth adopting:

CurrentUser = Annotated[User, Depends(current_user)]

def me(user: CurrentUser):
    ...

The dependency turns a named type, reusable across each endpoint. The signature reads as plain Python. It's the same idea as the constrained types from the Pydantic track, applied here.

Where they live

A dependencies.py beside your routers is the usual home, for the same reason a types.py is. A few routers need the same ones. A shared file is where they can be found.

Reading that file should tell you what the use's endpoints are allowed to assume. A user, a DB session, a tenant, a set of filters. That is a handy summary to have in one place.

Mistakes people make with Dependency Injection

Wrapping a thing trivial. Depends on a function returning a constant is indirection with no gain. Import the constant.

Putting the endpoint's work in a dependency. A dependency supplies what the endpoint needs. It shouldn't *be* what the endpoint does. The test. Can you say "this endpoint needs an X"? Then it's a dependency. "This endpoint does Y" isn't.

Returning a dict where a model belongs. user["name"] has no completion and no checking. A dependency returning a model gives each handler field access and states its contract.

given the cache is global. It's per request. Nothing lives to the next one - which is correct. A cached user leaking across requests would be a serious bug.

**Expecting Depends to hide params.** all a dependency declares appears in the endpoint's documented params. That is a feature. It means adding a needed param to a shared dependency is a breaking change for each endpoint using it.

Doing slow work without noticing where. A dependency runs on each request to each endpoint that declares it. A lookup that seemed cheap on one route is times'd by all that shares it.

The Annotated form

Worth adopting early, because it changes how the signatures read:

CurrentUser = Annotated[User, Depends(current_user)]

@app.get("/me")
def me(user: CurrentUser):
    ...

The dependency turns a named type. The signature is plain Python with no default-arg trick, the need is declared once and reused. A reader sees a type rather than a call.

It's the same idea as the constrained types from the Pydantic track. For a code with a few dependencies it's the tidier spelling.

What this replaces in other frameworks

It's worth seeing what the same job looks like elsewhere. It shows why the FastAPI version is so small.

A decorator - @login_required - is the Flask-shaped answer. It works. The need is invisible to the function's signature. Nothing docs it, the value has to be smuggled in through a global request object, and composing two decorators means caring about their order.

A holder - the Spring or .NET answer - registers setups against interfaces and resolves them by type. Powerful. It needs config, wiring and a mental model of its own.

A base class - class MyView(AuthenticatedView) - ties the need to class chain. An endpoint needing two unrelated things needs multiple class chain.

FastAPI's version is a default arg. There's nothing to register, nothing to configure, no sorting to reason about. The need is written where a reader and the schema gen both look. That is a truly good trade. It's why the feature is worth using rather than routed around.

Testing an endpoint that has dependencies

The payoff arrives in the test file, and it's worth previewing before the replaces module.

An endpoint declaring Depends(get_db) and Depends(current_user) can be tested without a DB and without a token. Both can be replaced at the app level. The handler is unchanged. Only what it depends on moves.

That is the hands-on arg for pushing needs into dependencies rather than reaching for them inside handlers. A handler that calls get_session() right can't be tested without a session. One that declares it can.

One habit worth forming

When you find yourself writing the same four lines at the top of a second handler, that is the moment.

Not the fifth handler, and not after a refactor - the second. Extracting it costs one function and one param, and each endpoint after that takes from the rule instead of copying it.

The failure mode of waiting isn't the duplication itself. It's that the copies drift. One checks the header case-insensitively, one strips whitespace, one returns a dict where the other returns a model. By the time somebody consolidates them there are four actions to merge and no way to know which was intended.

Dependency Injection: summary

Depends(fn) calls a function and passes the result. The function's own params are request params. A dependency can read anything a handler can - and all it declares appears in the endpoint's documented contract.

Raising inside one stops the request before the handler runs. This Is why authentication belongs there rather than in a body somebody can forget to write. Results are cached per request. A dependency reached by a few paths is called once.

Prefer returning a model over a dict, adopt the Annotated spelling for anything reused. Keep dependencies to "this endpoint needs an X" rather than "this endpoint does Y".

Why it is the framework's best idea

Of all FastAPI adds on top of Starlette and Pydantic, this is the part with no equal elsewhere that is this small.

A decorator hides the need from the signature. A holder needs registration and config. A base class ties the need to class chain. Middleware applies to all and docs nothing.

Depends is a default arg. It needs no setup, composes without sorting rules, works with any callable, appears in the made schema. Can be replaced in a test with one dict set.

The result is that "what does this endpoint need?" is answerable by reading its signature. "what happens if it isn't there?" is answerable by reading one function. Those two properties are most of what makes a large FastAPI use stay legible.

A closing thought

The habit this module is really teaching isn't Depends. It's declaring what you need instead of reaching for it.

A handler that calls get_session() in its body has acquired a thing. A handler that declares session: Session = Depends(get_session) has stated a need and let a thing else meet it. The first can't be tested without a DB, documented, or reused. The second is all three.

That gap is older and larger than this framework. FastAPI's part is making the declaring version shorter than the acquiring one. This Is the only solid way to get people to prefer it.

Two rules

Extract on the second case, not the fifth. The cost is one function. The cost of waiting is four copies that have quietly diverged.

Dependencies supply, they don't perform. "This endpoint needs an X" is a dependency. "this endpoint does Y" is the handler's job.

Questions people ask about Dependency Injection

What is a dependency in FastAPI? Any callable you pass to Depends(). FastAPI calls it for each request and passes the result to your function.

Does a dependency see the request? Yes. It can declare the same parameters an endpoint can: query, headers, cookies, and body.

Is a dependency called once per request or once per use? Once per request. If several dependencies share it, the result is cached for that request unless you pass use_cache=False.

Can a dependency stop the request? Yes. If it raises HTTPException, the endpoint never runs and the client gets that error.

What is Annotated used for here? Annotated[User, Depends(get_user)] lets you store the dependency in a reusable type alias, so signatures stay short.

How does this help testing? You can replace any dependency with a fake using dependency_overrides, so tests need no real database or login.

Where to go after Dependency Injection

Dependencies that need to clean up after themselves - a session that must be closed whether the handler succeeded or raised - which is what yield is for. Where the transaction pattern comes from.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What is a FastAPI dependency?

  2. Two parameters depend on the same function. How many times is it called?

  3. Why is a dependency the right place for authentication?

  4. Do a dependency's parameters appear in the API documentation?

Cheat sheet

Dependency Injection

Declare what an endpoint needs with Depends, let FastAPI resolve and inject it, and see how testing and swapping implementations get simpler.

FASTAPI · vizlearn.in/fastapi/dependency_injection.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.