Router and Global Dependencies

Protecting a whole section without every endpoint repeating the declaration - and the one endpoint that would otherwise forget.

Overview

Three places to declare one

Some rules apply to many endpoints at once. Every route in the admin section needs an admin user. Every route in the app might need an API key. Repeating the same dependency on each handler works, but it is tedious, and it creates a risk: the one endpoint you forget is the one left unprotected.

FastAPI lets you declare a dependency at a higher level, so it covers a whole group of routes. You can attach it to a single decorator, to an entire router, or to the whole application. The framework runs it for every matching request, before the handler.

This article starts with the three places you can declare one and how each differs in reach. It then explains an important detail: a dependency declared this way runs, but its value is not injected into your handler. If you need the value, you must still ask for it in the function. That distinction confuses many readers at first.

A section follows on the argument for the router form, which is usually the best balance. It protects a whole section by default, and a missing line in one handler cannot undo it. The article ends with practical advice on when to use each level, based on how many routes share the rule and how likely it is to change.

Read this if you care about security that does not depend on anyone's memory.

There is a security angle that makes this topic worth more than its length suggests. Protection that depends on every developer remembering to add a line will fail eventually. Protection that is built into the structure of the app does not. A new endpoint added to a protected router is covered from its first request, even if its author never thought about authentication. That is a far safer default. The article also looks at the cost, which is that a rule applied broadly is harder to opt out of, and shows how to handle the few endpoints that need to be public. Login and health checks are typical examples. Each section is short and ends with a habit you can apply straight away. You finish knowing which level suits each rule, and how to confirm in a test that the protection is really in place.

A dependency can be attached at three levels, all with the same arg:

FastAPI(dependencies=[Depends(fn)])                 # every route
APIRouter(dependencies=[Depends(fn)])               # every route in it
@app.get("/x", dependencies=[Depends(fn)])          # one route

They stack. All three run for a request that matches, outermost first. Each adds a rule rather than replacing one.

Worth knowing

dependencies=[Depends(fn)] on an APIRouter applies to every route in it.
The same argument works on FastAPI() for the whole application, and on a route decorator for one endpoint.
The return value is not injected — these run for their effect. A handler that needs the value declares its own Depends.
Because of per-request caching, declaring it twice still calls the function once.
App, router and route dependencies stack and run outermost first. Each layer adds a rule.
The real argument for the router form: a route added six months later is protected without anyone remembering to protect it.

Router and Global Dependencies

Protecting a section without each endpoint repeating itself - and the endpoint that would else be forgotten.

A dependency on the router

Declared once, applied to every route in it. The value is not injected — it runs for its effect.

example_01.pyFastAPI
Output

Application-wide

The same list on FastAPI() applies to every route in the app, including ones added later.

example_02.pyFastAPI
Output

On a single route

The decorator takes the same list, for a rule that applies to one endpoint and whose value the handler does not need.

example_03.pyFastAPI
Output

The value is not passed

That is the trade. A handler needing the user declares its own Depends, and the cache means it still runs once.

example_04.pyFastAPI
Output

They stack

App, router and route dependencies all run, outermost first. Each layer adds a rule rather than replacing one.

example_05.pyFastAPI
Output

The endpoint that would have forgotten

The argument for putting it on the router: a new route added later is protected without anyone remembering to protect it.

example_06.pyFastAPI
Output

The value is not injected

This is the gap from a param-level Depends, and the one thing to note.

A dependency declared this way runs for its effect. It can read headers, validate, raise, record an audit entry. But what it returns is discarded. There's no param to receive it.

So a handler that really needs the user still declares it:

router = APIRouter(dependencies=[Depends(current_user)])

@router.get("/who")
def who(user: User = Depends(current_user)):
    ...

That looks like a copy and isn't. Per-request caching means the function runs once. The router-level declaration promises the check happens on each route. The param-level one gets the value where it's needed.

The argument for the router form

It isn't brevity. It's that a route added later is covered by default.

Protecting endpoints one at a time works just right until somebody adds the fourteenth one and doesn't know the rule, or knows it and is in a hurry. That endpoint is then unprotected, nothing fails, no test covers it because no one wrote a test for a route they didn't know needed one. The gap is found later by someone who was looking.

The router form inverts that: losing isn't possible. The protection is a property of the section rather than of each endpoint. Adding a route to /admin makes it an admin route.

The last editor above shows both setups side by side, with one endpoint in each. The grouped one is safe. The single one has a hole.

When to use which level

use-wide for cross-cutting concerns that truly apply to all. Request identifiers, tracing, a global rate limit. Be careful. A health check that now needs an API key is a common self-inflicted outage. Public endpoints stop being public. If more than a couple of routes need an exemption, this is the wrong level.

Router-level for a section with a shared rule. This is the sweet spot. Where most real use lives: /admin needs an admin, /inner needs a service token.

Route-level for a rule truly exact to one endpoint whose value the handler doesn't need. Recording an audit entry, checking a feature flag, enforcing an idempotency key.

What it does to the documentation

params declared by these dependencies still appear on each affected endpoint. As far as a caller is concerned the endpoint needs them.

So a router needing an x-api-key header docs that header on all of its routes. That is right. It's a small arg for the router form over middleware. This Would enforce the same rule invisibly.

The responses= arg pairs with this: a router that can 401 should say so once, at the router, rather than on each route.

Dependencies or middleware?

Both can enforce a thing across many routes. The choice comes up as soon as either does.

A dependency knows about routing. So It applies to a chosen set. It can declare params. This Appear in the schema. It integrates with the error handling you already have. It can be overridden in tests. It runs after routing, so it knows which endpoint matched.

Middleware runs on each request including unmatched ones, before routing. It can't declare params and doesn't appear in the docs.

The rule that follows: if it's about *this endpoint* or *this section*. Authentication, permissions, check. It's a dependency. If it's about *each request even so of route*. CORS, packing, a request id, timing — it's middleware.

Reaching for middleware to do authentication is a common early choice and often regretted. The rule turns invisible to the schema, awkward to exempt one route from. Hard to replace in a test.

Testing them

dependency_overrides works on these just as on param-level ones. This Is the next module and is what makes a router-wide auth need pleasant rather than tiresome to test.

Mistakes people make with Router and Global Dependencies

use-wide authentication. It catches the health check, the metrics endpoint and the docs. The resulting outage is self-inflicted. If more than a couple of routes need an exemption, the level is wrong.

Expecting the value to be injected. These run for their effect. A handler that needs the value declares its own Depends. The cache means the function still runs once.

Using middleware for it instead. Middleware runs before routing, can't declare params, is invisible to the schema and is awkward to exempt one route from or replace in a test.

Protecting routes one at a time. It works until the fourteenth route is added by somebody who doesn't know the rule. Nothing fails and no test covers it.

**Losing responses= on the router.** A section that can 401 should doc it once, at the router, rather than on each route or nowhere.

given order doesn't matter. App, then router, then route. Each layer can lean on the ones outside it having passed. This Is what makes layered rules safe.

Where the boundary sits

The clean division, stated once:

Dependencies are about endpoints. They know which route matched, declare params that reach the schema, integrate with your exception handlers. Can be overridden in tests. Authentication, permissions, request-scoped resources.

Middleware is about requests. It runs on all including unmatched paths, before routing. Knows nothing about your endpoints. CORS, packing, request identifiers, timing.

Choosing by that question rather than by convenience keeps both simple.

Choosing the level, concretely

A short choice method that avoids the common mistakes.

Does each route without exception need it, including health checks and docs? Then use level. Very few things qualify - a request identifier, tracing.

Does a coherent section need it? Router level. This is where most real use lives. It's the level that lives a route being added later.

Does just one endpoint need it, and the handler doesn't want the value? Route level.

Does the handler need the value? A param, not any of these - and if the section also needs the promise, declare it in both places and let the cache make it one call.

The mistake worth naming again is the first. An use-level authentication dependency reads as tidy and takes out /health with it. This Is found by a load balancer at the worst possible moment.

Documenting a protected section

Two args belong on the router beside the dependency.

responses={401: {"description": "Missing or invalid key"}} docs the failure once for each route in the section.

tags=["admin"] groups them, so a reader of the docs sees the protected endpoints together rather than scattered among the public ones.

Both are single args, and together they turn "these routes need a key" from a thing a caller discovers by being rejected into a thing the schema states.

A note on ordering and errors

Because the layers run outermost first, the error a caller sees is from the outermost layer that failed.

That is often right - a request with no API key should be told that, not told it lacks a permission it couldn't have been checked for. It does mean the layers should be sorted from most general to most exact. This The app/router/route nesting gives you for free.

Within a single dependencies=[...] list, the entries run in order. A cheap check should come before an costly one. There's no point querying a permissions table for a request whose token is missing.

Documenting what a section requires

Worth restating because it's the gap between a section a caller can use and one they've to reverse-engineer.

A protected router should carry three things: the dependency that enforces the rule, a responses entry showing the failure. A tags entry grouping the routes.

With those, the made docs shows a labelled group of endpoints, each documenting the header it needs and the 401 it can return. Without them the endpoints still work and a caller discovers the need by being refused. This Is a worse first experience than any amount of prose can make up for.

The decision in one line

Put it on the router.

use-level catches the health check. Route-level is forgotten by whoever adds the fourteenth endpoint. The router is the level that matches how people really think about an API - "all under /admin needs an admin" - and it's the only one of the three where adding a route later can't create a hole.

Reach past it only when the rule truly applies to each request without exception, or truly applies to just one endpoint.

And the one to remember

The return value is discarded. These run for their effect.

When a handler needs the value as well, declare it again as a param and let the per-request cache collapse the two into one call. That looks redundant the first time you write it and isn't. The router declaration promises the check on each route. The param gets the value where it's used.

Router and Global Dependencies: summary

dependencies=[Depends(fn)] attaches a dependency to one route, a router, or the whole use. They stack and run outermost first. Their return values are discarded - a handler needing the value declares its own. The cache keeps it to one call.

The router level is where most real use belongs. The reason isn't brevity. A route added later is protected without anyone remembering to protect it.

Keep it out of the use level unless it truly applies to the health check too. Use a dependency rather than middleware when the rule is about endpoints rather than about each request.

Summary, in one line

Put shared rules on the router. Here A route added next year takes from them. Declare the value again as a param when the handler needs it. Let the per-request cache make that one call rather than two.

Two rules

Prefer the router level. It's the only one where a route added later can't escape the rule.

Declare it again as a param when the handler needs the value. The cache makes that one call. The two declarations mean two other things: the section's promise, and this handler's need.

Where it sits in the tier

The other modules make a dependency on hand to an endpoint that asks for it. This one makes it apply to endpoints that didn't.

That gap matters most for security. Here The failure is silent. No one notices an unprotected route until somebody is looking for one.

Questions people ask about Router and Global Dependencies

Where can I declare dependencies besides each endpoint? On the decorator, on an APIRouter, in include_router, and on the FastAPI app itself.

Do these dependencies pass a value to my function? No. They run for side effects like checks and raise on failure, but the return value is not injected.

How do I protect a whole section of the API? Add dependencies=[Depends(require_user)] to the APIRouter. Every route on it then needs a signed-in user.

What is the order they run in? App-level first, then router, then decorator, then the endpoint's own parameters.

Dependencies or middleware? Use a dependency when you need route-aware logic, validation, or the docs to reflect it. Use middleware for every request regardless of route.

How do I test a router with protected routes? Override the dependency in tests with dependency_overrides, so no real credentials are needed.

Where to go after Router and Global Dependencies

Replacing a dependency for a test - the piece that makes all in this tier testable without a DB, a token service or a network.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What happens to the return value of a router-level dependency?

  2. What is the main argument for a router-level dependency over per-route?

  3. In what order do app, router and route dependencies run?

  4. Authentication across a section: dependency or middleware?

Cheat sheet

Router and Global Dependencies

Attach a dependency to a router so every endpoint under it runs the same check, and spot the unprotected endpoint the pattern catches.

FASTAPI · vizlearn.in/fastapi/router_and_global_dependencies.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.