# How do I keep FastAPI routes visible to coding agents?

> Rule AR-33: Declare dependencies as typed functions used through Annotated[..., Depends(fn)], and write routes as decorated functions on an APIRouter. Don't build routes in loops from strings.

FastAPI is built around ordinary Python functions with type annotations, which is why editors and agents read it well. A route written as `@router.get("/items")` over a named function is easy to find, and a dependency passed as a function object is a reference every tool follows. The pattern stops being readable when route functions are looked up by name from a table, or when dependencies are made from strings and `lambda`s. Keep to the plain form and the framework's strengths carry over to the tools.

## Why it happens
FastAPI's docs describe a dependency as "just a function that can take all the same parameters that a _path operation function_ can take". You attach it with `Depends`, and with `Annotated` you can store the declaration under a name:

> "because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places ... This is just standard Python, it's called a 'type alias'."

The docs state the benefit: "the type information will be preserved, which means that your editor will be able to keep providing you with autocompletion, inline errors, etc. The same for other tools like `mypy`." And `Depends` takes the function itself: "You don't call it directly (don't add the parenthesis at the end), you just pass it as a parameter to `Depends()`."

That is the visible form. A name in `Depends(get_db)` is a reference a rename tool and a caller search follow. Routing has the same property. The bigger-applications page says "You can think of `APIRouter` as a 'mini `FastAPI`' class", with prefix, tags, responses and dependencies set on the router, and `app.include_router()` adding the router's routes to the app. Every route is a decorated function in a module, and every router is a named object included by name.

What hides routes and dependencies:

- **A route table.** Registering handlers from a dict of strings in a loop (`getattr(handlers, name)`) is the dispatch pattern of AR-07 applied to routing. A search for `list_items` finds the definition, and nothing says it serves `GET /items`.
- **Dependencies that come from a factory or a string.** A dependency chosen with `globals()[name]` or built per call in a function has no stable name. The testing docs say `app.dependency_overrides` is keyed by the original dependency function: "you put as a key the original dependency (a function), and as the value, your dependency override". A function object created inside a factory on each call is a different key each time (the page calls `dependency_overrides` "a simple `dict`" keyed by the function itself and never says "identity" or "equality"; a Python dict keyed by function objects matches the same object, which is the standard dict behaviour rather than a FastAPI statement; https://fastapi.tiangolo.com/advanced/testing-dependencies/).
- **The old default-value form.** `commons: dict = Depends(common_parameters)` works, but the docs and ruff's `FAST002` prefer `Annotated`, which also keeps the parameter's own type.
- **Dependencies hidden in decorators and routers.** `@app.get("/items/", dependencies=[Depends(verify_token)])` and `APIRouter(dependencies=[...])` run for each request, and "their value (if they return any) won't be passed to your path operation function". A route function can look unauthenticated and not be. That is a feature, and the reason to keep the router declaration in the file the reader opens.

## Bad and good
A service exposes read endpoints that all need a database session.

```python
# bad: app/main.py
from fastapi import Depends, FastAPI

from app import handlers

app = FastAPI()

ROUTES = {
    "list_items": "/items",
    "get_item": "/items/{item_id}",
    "list_users": "/users",
}

for name, path in ROUTES.items():
    app.get(path)(getattr(handlers, name))


def get_db(kind: str = "primary"):
    return globals()["open_" + kind]()


@app.get("/orders")
def list_orders(db=Depends(get_db)):
    ...
```

An agent asked to add a parameter to `handlers.get_item` finds the definition and no route that serves it. The route table is data, `getattr` builds the handler name.

```python
# good: app/routers/items.py
from typing import Annotated

from fastapi import APIRouter, Depends

from app.deps import Session, get_session

router = APIRouter(prefix="/items", tags=["items"])

SessionDep = Annotated[Session, Depends(get_session)]


@router.get("/")
async def list_items(session: SessionDep) -> list[Item]:
    return await item_repo.list(session)


@router.get("/{item_id}")
async def get_item(item_id: int, session: SessionDep) -> Item:
    return await item_repo.get(session, item_id)
```

```python
# good: app/main.py
from fastapi import FastAPI

from app.routers import items, users

app = FastAPI()
app.include_router(items.router)
app.include_router(users.router)
```

Each route is a decorated function, the dependency is the named `get_session`, and `SessionDep` carries the type. A search for `get_session` lists every route that uses it through `SessionDep`, and `app.dependency_overrides[get_session]` in tests replaces the same object the routes reference.

## Trade-offs and when the dynamic form is fine
- **Loops are less typing.** Fifty near-identical endpoints generated from a spec are shorter as a loop. If the routes come from an external spec, generate the Python files (AR-23) and commit them, so tools read decorated functions.
- **`include_router` costs nothing at runtime.** The docs: "You don't have to worry about performance when including routers", so the split into modules carries no speed penalty.
- **Router-level dependencies move requirements out of the route.** They save repetition for auth. Name the router's dependency in the router constructor, where the route's file reader sees it.
- **Including one router twice** with different prefixes is documented, for `/api/v1` and `/api/latest`. Mind that a route then has two URLs for one function.
- **`Annotated` needs Python 3.9 or newer** (typing_extensions before that), per ruff's `FAST002` page.

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `FAST002` fast-api-non-annotated-dependency (the `FAST` family; not marked preview in the rules index) | `Depends(...)`, `Query(...)` and similar used as default values instead of inside `Annotated`. The fix is always unsafe | 2026-10-30 |
| ruff | `FAST001` fast-api-redundant-response-model, `FAST003` fast-api-unused-path-parameter | a `response_model` "with the same type as the return type" (https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/), and a route that declares "path parameters in the route path that are not included in the function signature" (https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter/). Neither is marked preview and neither is in ruff's default set, so select `FAST` | 2026-10-30 |
| FastAPI | `app.dependency_overrides` in tests | a test that replaces the exact dependency function the routes use; overriding a different function silently changes nothing | 2026-10-30 |

No tool in the pages read flags a route registered in a loop. It is a review rule.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
## FastAPI
- Routes are decorated functions (@router.get(...)) on an APIRouter, in the module named for the resource. Routers are included in main.py by name with app.include_router.
- Don't register routes in loops, or look up handler functions by name (getattr, globals). If routes come from a spec, generate the .py files and commit them.
- Dependencies are plain typed functions. Declare them as Annotated[Type, Depends(fn)] and give repeated ones a type alias (SessionDep). Don't use Depends(...) as a default value.
- Don't create dependency functions inside other functions per call. Tests override dependencies with app.dependency_overrides[original_function], so the function must be one stable module-level name.
- Before changing a dependency's parameters or return type, search for its name, its Annotated aliases and every dependency_overrides key that uses it.
- Reset app.dependency_overrides at the end of each test.
```

## Edge cases
- **Router-level and app-level dependencies.** `FastAPI(dependencies=[...])` and `APIRouter(dependencies=[...])` apply to every route beneath. When changing one, check every route it reaches.
- **Overrides in `include_router`.** The testing docs say an override works wherever the dependency is used, including "a `.include_router()` call".
- **Parameterised dependencies.** A class with `__call__`, or a factory returning a function, is legitimate. If the factory runs at import, bind the result to a module-level name and use that name as the override key.
- **Sync and async.** Docs: "You can use `async def` or normal `def`." Mixing is fine. Changing a dependency from `def` to `async def` doesn't change callers.
- **Return values of decorator dependencies are unused.** Don't move a dependency whose value you read into the decorator list.
- **Reset overrides.** Docs: `app.dependency_overrides = {}` resets them. A test that sets one and never clears it changes later tests, which is the fixture problem of AR-34.
- **Generated routes in chimera form.** See CH-05 in the index (routes generated in a loop from a dict of string names), not yet published.

## What LLMGRAPH shows
A handler registered in a loop prints 'plus 1 dynamic call nearby that could reach anything'. A decorated route handler prints 'no calls to it found in this repo'; LLMGRAPH does not mark route handlers as entry points or list the routes that use a dependency.

## References
FastAPI (fetched 2026-10-30):
- Dependencies tutorial (`Depends`, `Annotated` type alias, sync and async): https://fastapi.tiangolo.com/tutorial/dependencies/
- Bigger applications (`APIRouter`, `prefix`, `dependencies`, `include_router`, performance): https://fastapi.tiangolo.com/tutorial/bigger-applications/
- Dependencies in path operation decorators: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/
- Testing dependencies with overrides: https://fastapi.tiangolo.com/advanced/testing-dependencies/
- `APIRouter` reference: https://fastapi.tiangolo.com/reference/apirouter/

Ruff (fetched 2026-10-30):
- FAST002: https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/ · rule list: https://docs.astral.sh/ruff/rules/

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: docs checks resolved
- 2026-10-30: remaining checks closed (claims reduced to documented behaviour)

## Related

- [AR-07: Why does my coding agent miss calls made through getattr?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-08: How do I write dispatch that type checkers can follow?](https://beta.llmgraph.in/agent-readable/ar-08.md)
- [AR-09: How should plugin registries be written for coding agents?](https://beta.llmgraph.in/agent-readable/ar-09.md)
- [AR-13: Why is Any a blind spot for coding agents?](https://beta.llmgraph.in/agent-readable/ar-13.md)

Last verified 2026-10-30
