# FastAPI Large-App Design: Building a 'Maintainable API' with APIRouter, Tiered Dependency Injection, and Project Structure

> A design guide for keeping a large FastAPI API maintainable. Explained with real code, faithful to the official Bigger Applications: APIRouter, the recommended project structure, and relative imports, plus 4-tier DI (global/router/decorator/argument), router→service→repository layering, avoiding circular imports, API versioning, docs maintenance, and testing.

- Published: 2026-06-26
- Author: 友田 陽大
- Tags: Python, FastAPI, アーキテクチャ設計, 依存性注入, 保守性
- URL: https://tomodahinata.com/en/blog/fastapi-project-structure-apirouter-dependencies-large-app-guide
- Category: Python backend
- Pillar guide: https://tomodahinata.com/en/blog/fastapi-production-async-pydantic-observability-guide

## Key points

- A single main.py breaks down the instant 'auth + multiple resources + DB' come together. Carve out an APIRouter per feature and split into the officially recommended app/ (main.py, dependencies.py, routers/, internal/)
- include_router can attach prefix/tags/dependencies/responses, and the original router isn't changed (you can reuse the same router with a different configuration). Nesting is also possible with router.include_router
- Design dependency injection in 4 tiers: global = FastAPI(dependencies=), feature group = APIRouter(dependencies=), side-effects only = the decorator's dependencies=[], using the return value = the function argument's Depends
- Keep the router thin, only at the HTTP boundary, and lean business logic to the service and I/O to the repository (SRP/ETC). Fix the dependency direction one-way and circular imports structurally don't occur
- Production finishing: API versioning (/api/v1), making docs an asset with openapi_tags while narrowing exposure in production with docs_url=None, isolating tests with a create_app factory, and running with fastapi dev / fastapi run

---

The first `main.py` is beautiful. `@app.get("/")` lines up, and it works in 5 minutes. But **the instant auth comes in, resources grow to 3, and a DB is plugged in**, that `main.py` becomes a 2,000-line ball of mud. Every time you add an endpoint, an existing one breaks, tests are slow, and a new member can't tell where to write what — **"works" and "maintainable" are different things.**

This article is a guide to building a **large, production-quality app structure** in FastAPI. While following the `APIRouter` and project structure that FastAPI's official "Bigger Applications" shows **faithfully to the latest spec**, it delves into the areas the official tutorial (being teaching material) doesn't touch — **when to split (YAGNI), router/service/repository layering, structural avoidance of circular imports, API versioning, docs hardening, and testability.** As the subject matter, I'll weave in decisions from an internal AI platform I built for a major domestic broadcaster ([a monorepo bundling multiple FastAPI services under a single convention](/case-studies/broadcaster-ai-content-platform). Unifying common auth, logging, and cache conventions into one, designed as a horizontally-deployable platform).

> **The rule of this article**: The structure and APIs are based on the **FastAPI official documentation (as of June 2026)**. In recent years FastAPI gained the **`fastapi` CLI (`fastapi dev` / `fastapi run`)** for development / production startup and a `[tool.fastapi]` entrypoint setting in `pyproject.toml`. This article conforms to this latest version. Since specs get revised, always confirm the latest behavior officially before going to production. As a design premise, also read together: **async usage, lifespan, and observability in the [production operation guide](/blog/fastapi-production-async-pydantic-observability-guide), and authentication / authorization in the [authentication guide](/blog/fastapi-authentication-oauth2-jwt-security-scopes-production-guide).** This article concentrates on "**how to arrange and maintain them.**"

---

## 0. First, the judgment: when to split (YAGNI)

Before structuring, it's important to have the judgment of **"don't split yet."** Carving `services/` `repositories/` into a 3-endpoint tool is over-engineering (a YAGNI violation) and conversely makes it harder to read.

Use the **signs** of splitting as the criterion.

- `main.py` **exceeded one screen (~200 lines).**
- **Resources (nouns) became 3 or more** (users / items / orders …).
- **Cross-cutting concerns** (auth, DB session, logging) started being **copy-pasted** into multiple handlers.
- **The team became 2 or more people** and started conflicting on the same file.

Conversely, if none of the above applies, **staying a single file is correct** (KISS). This article targets "an app that has come to the stage where it should be split."

---

## 1. `APIRouter`: carve out routes per feature

The minimal unit of splitting is the **`APIRouter`.** As the official wording goes — **`APIRouter` works exactly like the `FastAPI` class.** All the same options are usable — parameters, responses, dependencies, tags. Thinking of it as a "mini FastAPI" is accurate.

```python
# app/routers/users.py
from fastapi import APIRouter

router = APIRouter()    # この機能（users）のルートを束ねる入れ物

@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]

@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}
```

Just change `@app.get` to `@router.get`. With this, the routes about `users` **cohere into one file** and disappear from `main.py` (SRP: the only reason this file changes is "a change to the user feature").

---

## 2. The officially recommended project structure

The standard layout the official docs show is this. **First building this shape correctly** becomes the foundation of everything.

```text
.
├── app                  # "app" は Python パッケージ
│   ├── __init__.py      # これで "app" がパッケージになる
│   ├── main.py          # アプリ本体（FastAPI() と include_router）
│   ├── dependencies.py  # 共有の依存（認証・共通ヘッダ検証など）
│   ├── routers          # "routers" はサブパッケージ
│   │   ├── __init__.py
│   │   ├── items.py     # items 機能のルート
│   │   └── users.py     # users 機能のルート
│   └── internal         # 社内/管理用など外に出さない機能
│       ├── __init__.py
│       └── admin.py
```

There are three points.

### 2.1 `__init__.py` is not an "omittable decoration"

Each directory's **`__init__.py` makes that directory a Python package.** It's because of this that imports like `from app.routers import items` hold. An empty file is fine, but **deleting it breaks the imports.**

### 2.2 Consolidate shared dependencies in `dependencies.py` (DRY)

**Dependencies used from multiple features**, like an auth check or common header validation, are bundled in one place.

```python
# app/dependencies.py
from typing import Annotated
from fastapi import Header, HTTPException

async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")

async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
```

### 2.3 Reference with relative imports

When referencing `dependencies.py` from `routers/items.py`, use a **relative import.**

```python
from ..dependencies import get_token_header
#   ^^  二つのドット = 親パッケージ（app）まで上がってから dependencies を見る
#   一つのドット（.）= 同じパッケージ内
```

> **The standard to avoid name collisions**: write both `from .routers.items import router` and `from .routers.users import router`, and **the latter overrides the former.** As in the official docs, **import the module itself** — `from .routers import items, users`, and reference it **namespace-qualified** as `items.router` / `users.router` when using it.

---

## 3. `include_router`: incorporate features into the body

Incorporate the carved-out routers in `main.py`. Being able to **attach prefix, tags, dependencies, and responses all at once** here is powerful.

```python
# app/main.py
from fastapi import Depends, FastAPI
from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])   # ← 全体に効く依存（第4章）

app.include_router(users.router)
app.include_router(items.router)

# admin は「組み込み時に」prefix・タグ・追加の依存・追加レスポンスを上書き付与する
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],     # /admin 配下すべてにトークン検証を強制
    responses={418: {"description": "I'm a teapot"}},
)

@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}
```

`prefix` / `tags` / `dependencies` / `responses` can be specified **both when creating the router (`APIRouter(...)`) and when incorporating it (`include_router(...)`).** Distinguish them: "settings specific to that feature" on the router side, "settings added due to the circumstances of where it's incorporated" on the include side.

> **An important official spec: the original router isn't changed.** Add a prefix or dependencies with `include_router`, and the **original `APIRouter` object stays unmodified.** So you can **reuse the same router in a different app with a different prefix and different dependencies.** It's a property that pays off when distributing a router like a shared library. Just note that `prefix` **doesn't take a trailing slash** (`/items`, not `/items/`).

### 3.1 Nest a router inside a router

A router can also be **incorporated into another router.** This becomes the key to the versioning (bundling `/api/v1`) described later.

```python
parent.include_router(child_router)   # APIRouter を APIRouter に入れ子にできる
```

---

## 4. Design dependency injection in "4 tiers"

FastAPI's true value is in **dependency injection (DI).** But what you agonize over in the field is not the feature but "**at what height to place this dependency.**" FastAPI provides **4 tiers.** Make this a map and design gets a lot easier.

| Tier | How to write it | Range of effect | Suited use |
| --- | --- | --- | --- |
| ① Global | `FastAPI(dependencies=[...])` | **All endpoints** | A premise common to all APIs (API-key validation, requiring a common header) |
| ② Router | `APIRouter(dependencies=[...])` / `include_router(dependencies=[...])` | **All of that feature group** | Force admin privilege under `/admin`, tenant resolution |
| ③ Decorator | `@router.get(..., dependencies=[...])` | **That one endpoint** | Validation / side effects not needing a return value (rate limit, specific permission check) |
| ④ Function argument | `param: Annotated[X, Depends(...)]` | **That one endpoint** | **Using the return value** (DB session, current user) |

The judgment is one line — **"Does the handler body use the return value?"** If yes, ④ (function argument). If not (validation / side effects only), one of ①–③. And **the wider the range you want it to take effect, the higher the tier** you place it.

### 4.1 ① Global dependency: a premise common to all endpoints

```python
# 全リクエストに先立ってヘッダ検証を走らせる（一箇所で全体に効く）
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
```

A dependency passed to `FastAPI(dependencies=[...])` is **executed on all of the app's path operations.** Declare "a premise mandatory in all APIs" once here, rather than scattering it across handlers (DRY).

### 4.2 ③ Decorator dependency: don't need the return value, but want it always executed

When "you want to validate, but don't use that result in the handler," the **decorator's `dependencies=[...]`** is optimal.

```python
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    # verify_token / verify_key は必ず実行される（失敗すれば例外で弾かれる）。
    # ただし戻り値はハンドラに渡らない（使わないので引数にも現れない）。
    return [{"item": "Foo"}, {"item": "Bar"}]
```

These dependencies can **throw exceptions like ordinary dependencies** (can return 4xx with `HTTPException`) and can read headers too. The difference is that **the return value is ignored.** Hang a "dependency you don't use" on a function argument, and the editor warns "unused argument," confusing the reader. **For side effects only, to the decorator rather than the argument** — this is a small but effective device for readability.

### 4.3 ② Router dependency: consolidate authorization at the router layer (most important)

What pays off most in practice is the **router-layer dependency.** When you want to force a permission check on **all endpoints** under `/admin`, writing it in each handler is a breeding ground for gaps. Attach it once to the router, and **forgetting to write it structurally doesn't happen.**

```python
from fastapi import APIRouter, Security
from app.core.security import get_current_active_user   # 認証ガイド参照

# /admin 配下の全ルートに admin スコープを強制（DRY・抜け漏れ防止）
admin_router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Security(get_current_active_user, scopes=["admin"])],
)

@admin_router.get("/reports")     # ← ここに認可を書かなくても admin が要求される
async def list_reports():
    ...
```

On the broadcaster's platform too, I **defined cross-cutting concerns like auth, logging, and rate limiting in one place as "a convention common to each service"** and horizontally deployed them to 5 FastAPI services. On the lumber-distribution B2B SaaS ([Minister of Economy, Trade and Industry Award-winning](/case-studies/lumber-industry-dx)), I **consolidated industry-based authorization at the router layer** and protected all 221 endpoints with a consistent policy — demonstrating **0 missing-authorization findings** in a third-party pen test. Authorization is production quality when protected by **a structure consolidated at the boundary**, not by "each handler's well-meaning if statement" (→ the [authentication / authorization guide](/blog/fastapi-authentication-oauth2-jwt-security-scopes-production-guide)).

---

## 5. Layering: keep the router thin, push logic to service / repository

From here is the **"beyond"** of the official tutorial. The official examples, for learning, write business logic **inside the handler (router).** This is correct for small scale, but breaks down at large scale — **a handler holding all of HTTP, business rules, and DB access** makes tests heavy and changes propagate (an SRP violation).

So split the responsibility into 3 layers.

- **router (the HTTP boundary)**: only the conversion of request/response. **Keep it thin.**
- **service (business logic)**: business rules. **Knows neither HTTP nor the DB driver.**
- **repository (I/O)**: only the input/output to the DB / external APIs.

```python
# app/repositories/user_repository.py —— I/O のみ。SQLの詳細はここに閉じる
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import User

class UserRepository:
    def __init__(self, db: AsyncSession) -> None:
        self._db = db

    async def get(self, user_id: int) -> User | None:
        return await self._db.get(User, user_id)

    async def add(self, user: User) -> User:
        self._db.add(user)
        await self._db.flush()
        return user
```

```python
# app/services/user_service.py —— 業務ルールのみ。HTTPもSQLも登場しない（純粋でテストしやすい）
from app.repositories.user_repository import UserRepository
from app.schemas.user import UserCreate, UserPublic

class UserService:
    def __init__(self, repo: UserRepository) -> None:
        self._repo = repo

    async def register(self, payload: UserCreate) -> UserPublic:
        # 例：業務ルール（重複チェック・初期ロール付与など）はここに集約する
        user = await self._repo.add(payload.to_model())
        return UserPublic.model_validate(user)
```

```python
# app/api/deps.py —— 「配線（DI）」を一箇所に集約する
from typing import Annotated
from fastapi import Depends
from app.core.db import DbDep                       # lifespan で作った session を注入（本番運用ガイド参照）
from app.repositories.user_repository import UserRepository
from app.services.user_service import UserService

def get_user_service(db: DbDep) -> UserService:
    # repository を生成して service に注入する。これがDIの結節点。
    return UserService(UserRepository(db))

UserServiceDep = Annotated[UserService, Depends(get_user_service)]
```

```python
# app/api/v1/routers/users.py —— router は薄い。HTTP境界の翻訳だけ
from fastapi import APIRouter, status
from app.api.deps import UserServiceDep
from app.schemas.user import UserCreate, UserPublic

router = APIRouter()

@router.post("/", response_model=UserPublic, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, service: UserServiceDep) -> UserPublic:
    # router の仕事は「受け取って、serviceに渡して、返す」だけ。業務ルールはここに無い。
    return await service.register(payload)
```

The **payoff** of this split is concrete.

- **Testability**: `UserService` can be verified with **a plain unit test** without standing up HTTP. Swap the `repository` for a fake and you don't even need a DB.
- **Extensibility (ETC)**: swap the DB from PostgreSQL to something else, and the change is confined to the `repository`. The service and router are untouched.
- **Type safety**: fix the layer boundaries with Pydantic schemas (`UserCreate` / `UserPublic`) and make input and output separate types (the [Pydantic guide for boundary validation](/blog/pydantic-v2-production-validation-type-safety)).

> **Don't overdo it (YAGNI)**: you don't need to mechanically carve 3 layers into every feature. **A thin-logic CRUD can be written directly in the router.** Grow a service only for "a feature where business rules have grown" — incrementalism is correct.

### 5.1 An advanced layout for large scale

Introduce the 3 layers and the directory grows from the official minimal form to the following (move to this shape **after it becomes necessary**).

```text
app/
├── main.py            # create_app・include_router・lifespan・middleware（第8章）
├── core/              # 全層から参照される土台（循環の起点にしない）
│   ├── config.py      # Settings（pydantic-settings）
│   ├── db.py          # engine・session factory・get_db（DbDep）
│   └── security.py    # 認証の依存（get_current_user 等）
├── api/
│   ├── deps.py        # DIの結節点（service生成・型エイリアス）
│   └── v1/
│       ├── router.py  # v1の全routerを束ねる
│       └── routers/   # users.py・items.py …（HTTP境界）
├── services/          # ビジネスロジック
├── repositories/      # I/O
├── models/            # ORMモデル
└── schemas/           # Pydanticスキーマ（入力/出力）
```

---

## 6. Prevent circular imports with "structure"

The **most common accident** in scaling up FastAPI is a circular import (`ImportError: cannot import name ...`). The cause is almost always "**the dependency direction has become bidirectional.**" This can be eradicated with discipline.

**The iron rule: flow dependencies in only one direction.**

```text
main → api(routers) → services → repositories → models / schemas
                  ↘ core(config / db / security) ← a foundation anyone may reference
```

- **Higher layers may import lower layers. Lower layers must not import higher layers.** The moment a `repository` imports a `router`, that's the seed of a cycle.
- **Place `core/` (config, DB, security) as the bottom-most foundation**, on the side imported by anyone. `core` doesn't import higher layers.
- **Don't import `main.py` from other modules.** A router doesn't need `app` (it uses `APIRouter`). If you get a design where "the router wants `app`," that's the dependency direction reversed — the correct direction is to **inject from the incorporating side (main)** with `include_router`.
- **A mutual reference solely for type hints** is resolved by avoiding runtime import.

```python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:           # 型チェック時だけ評価され、実行時には import されない
    from app.services.user_service import UserService

def handler(service: "UserService") -> None: ...
```

As long as you keep the dependency direction one-way, a cycle **simply can't occur in the first place.** This isn't a story of "being careful" but of "**making it impossible by structure.**"

---

## 7. API versioning: evolve without breaking

For a public API, **backward compatibility** is the lifeline. You want to change the response shape of `/users` but can't break existing clients — in such cases, **run versions in parallel.** The "nest a router in a router" from Section 3.1 can be used as-is.

```python
# app/api/v1/router.py —— v1 の全機能を1つの router に束ねる
from fastapi import APIRouter
from .routers import users, items

api_router = APIRouter()
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
```

```python
# app/main.py —— /api/v1 配下にまとめて組み込む
from app.api.v1.router import api_router as v1_router
# from app.api.v2.router import api_router as v2_router   # v2 を足しても v1 は無傷

app.include_router(v1_router, prefix="/api/v1")
# app.include_router(v2_router, prefix="/api/v2")
```

For a new version, **grow a new router in a separate directory (`api/v2/`) and run it in parallel at `/api/v2`.** Because you don't touch v1's code, **existing clients break not one line** (ETC). Share common logic via service/repository **across both versions**, and keep what changes to only "how HTTP is presented (router/schema)" (DRY).

> **An alternative: mounting a sub-app.** If you want to make it a completely independent separate app (separate docs, separate lifecycle), there's also the hand of mounting a whole sub-application with `app.mount("/api/v2", subapp)`. But **for many cases, nesting routers is enough** (KISS). Ask yourself "is it really an independent app?" before choosing mount.

---

## 8. Consolidate settings, lifespan, and exception handlers (the create_app factory)

Create `app = FastAPI()` globally in `main.py` and **side effects run the instant it's imported** (settings load, connections), making it hard to swap in tests. Fold this into a **factory function** and the assembly can be surveyed at a glance, and you can create **a fresh app** per test.

```python
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import Settings, get_settings
from app.core.lifespan import lifespan          # DBプール等の確保（本番運用ガイド参照）
from app.core.errors import register_exception_handlers
from app.api.v1.router import api_router as v1_router

tags_metadata = [
    {"name": "users", "description": "ユーザーの作成・取得。**ログイン**もここ。"},
    {"name": "items", "description": "アイテム管理。"},
]

def create_app(settings: Settings | None = None) -> FastAPI:
    settings = settings or get_settings()
    app = FastAPI(
        title=settings.app_name,
        version=settings.version,
        lifespan=lifespan,
        openapi_tags=tags_metadata,
        # 本番では docs/openapi を閉じて API 構造の露出を絞る（後述）
        docs_url="/docs" if settings.docs_enabled else None,
        redoc_url=None,
        openapi_url="/openapi.json" if settings.docs_enabled else None,
    )
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.cors_origins,     # "*" は使わない（認証ガイド参照）
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    register_exception_handlers(app)             # 例外→HTTPの翻訳を一箇所に
    app.include_router(v1_router, prefix="/api/v1")
    return app

app = create_app()       # ASGIサーバ（fastapi run / uvicorn）が参照するエントリポイント
```

**The factory's payoff**: in tests, with `create_app(test_settings)` you can newly generate **an app isolated from production** every time. Because there are no global side effects, contamination between tests doesn't occur either (testability).

---

## 9. Make documentation an "asset" (and narrow exposure in production)

FastAPI's automatic docs are a powerful asset. Just by tidying up **the tags' descriptions and metadata**, `/docs` becomes a readable API spec.

```python
app = FastAPI(
    title="ChimichangApp",
    summary="一行サマリ（OpenAPI 3.1 / FastAPI 0.99.0+）",
    description="Markdownが使える詳細説明。**太字**やリストも可。",
    version="1.4.0",
    contact={"name": "API Support", "url": "https://example.com/contact", "email": "api@example.com"},
    license_info={"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"},
    openapi_tags=tags_metadata,    # ← この **リストの順序が docs の表示順**になる
)
```

Each element of `openapi_tags` can have `name` (required), `description` (Markdown OK), and `externalDocs`. And **the order you line them up in the list becomes the section order of the docs UI as-is.** Rather than a messy alphabetical order, you can line them up in **a business order natural for the reader.**

> **[Security] Don't expose docs defenselessly in production**: `/docs`, `/redoc`, and `/openapi.json` **publish the API's entire structure (all endpoints, all schemas).** For an internal API or an authenticated API, this can be a map for an attacker. In production, the standard is to **disable** them with `docs_url=None` / `redoc_url=None` / `openapi_url=None`, or to **hide them behind authentication** (branching by environment as in Section 8). Set `openapi_url=None` and the docs UI is also disabled at the same time.

---

## 10. Running and testing: the `fastapi` CLI and per-router verification

### 10.1 Startup: `fastapi dev` for development, `fastapi run` for production

FastAPI now has a dedicated CLI. **Development is `fastapi dev` with reload, production is `fastapi run` without reload** (driving uvicorn internally).

```bash
fastapi dev app/main.py     # 開発：自動リロード（既定は 127.0.0.1:8000）
fastapi run app/main.py     # 本番：リロード無し（既定は 0.0.0.0:8000）
```

The entrypoint can be written in `pyproject.toml` (`fastapi run` auto-discovers the app).

```toml
[tool.fastapi]
entrypoint = "app.main:app"   # from app.main import app と同義
```

For production worker multiplexing and graceful shutdown, follow [Section 9 of the production operation guide](/blog/fastapi-production-async-pydantic-observability-guide) (a process manager + multiple workers, cleanup in `lifespan`).

### 10.2 Testing: per-router + `dependency_overrides`

The biggest payoff of structuring is **testability.** Create an isolated app with `create_app` and swap dependencies.

```python
# tests/test_users.py
import pytest
from fastapi.testclient import TestClient
from app.main import create_app
from app.api.deps import get_user_service

@pytest.fixture
def client():
    app = create_app(test_settings())             # 本番から隔離した新鮮なアプリ
    # service を「フェイク」に差し替える → DB を立てずに router の契約だけ検証できる
    app.dependency_overrides[get_user_service] = lambda: FakeUserService()
    yield TestClient(app)
    app.dependency_overrides.clear()              # テスト間の汚染を防ぐ

def test_create_user_returns_201(client):
    res = client.post("/api/v1/users/", json={"email": "a@example.com", "display_name": "A"})
    assert res.status_code == 201
    assert "id" in res.json()
```

There are two points. **(1) The `service` layer can be unit-tested without a DB** (the recovery of Section 5's split). **(2) The router can swap the service for a fake with `dependency_overrides` and verify only the HTTP contract (status, schema) fast.** Layering and DI connect directly to the test strategy.

---

## 11. Summary: a maintainable-FastAPI structure cheat sheet

A quick-reference table for when you're unsure.

- **The split judgment**: split when `main.py` exceeds 200 lines / there are 3 or more resources / cross-cutting concerns start being copy-pasted (YAGNI: until then, one file is fine).
- **APIRouter**: carve out per feature with `router = APIRouter()`. A "mini FastAPI" with the same options as the `FastAPI` class.
- **Structure**: the official `app/` (`main.py`, `dependencies.py`, `routers/`, `internal/`). `__init__.py` in each dir. Relative imports (`.` / `..`). **Import the router as a module** to avoid name collisions.
- **include_router**: attach `prefix`/`tags`/`dependencies`/`responses`. **The original router is unmodified** and reusable. Nest with `router.include_router`.
- **DI 4 tiers**: global = `FastAPI(dependencies=)`, feature group = `APIRouter(dependencies=)`, side-effects only = the decorator's `dependencies=[]`, **using the return value** = the function argument's `Depends`. The judgment is "do you use the return value, and how wide a range to affect."
- **Layering**: thin router, business in service, I/O in repository (SRP/ETC). Grow a service only for a feature where logic has grown (YAGNI).
- **Circular imports**: dependencies one-way (`main→api→services→repositories→models`, `core` is the foundation). Type-only references with `TYPE_CHECKING`.
- **Versioning**: bundle `/api/v1` with nested routers, run `/api/v2` in parallel. Share common logic via the service. For complete independence, `mount`.
- **docs**: line them up in `openapi_tags` order. **In production, narrow exposure with `docs_url=None` etc.**
- **Run/test**: `fastapi dev` (development) / `fastapi run` (production), `[tool.fastapi] entrypoint`. Isolated tests with the `create_app` factory + `dependency_overrides`.

---

FastAPI is a "works in 5 minutes" framework, but whether you can **still touch it 6 months later** is decided by the initial structure. **Cohering features into routers, placing cross-cutting concerns at the correct height of dependency, splitting logic into layers, and fixing the dependency direction one-way** — none of these is flashy, but this accumulation builds "**an API that doesn't break down even when endpoints grow to 200.**"

On the broadcaster's internal AI platform, I designed **a monorepo bundling multiple FastAPI services under a single auth, logging, and cache convention**, and on the lumber-distribution SaaS I **maintained all 221 endpoints with a consistent structure** and demonstrated 0 missing-authorization findings in a third-party pen test. With generative AI (Claude Code) as a partner, my way of proceeding is to build **fast and cheap, alone** while ensuring quality with conventions and tests.

**"This FastAPI API of mine — features grew and main.py is suffering," "how should I introduce versioning or layering?" — from that design judgment through implementation, testing, and operation, I accompany you end to end.** Even from the requirements-organizing stage, feel free to consult me.

---

### Reference (official documentation)

- [Bigger Applications - Multiple Files (FastAPI)](https://fastapi.tiangolo.com/tutorial/bigger-applications/) — `APIRouter`, project structure, `include_router`, relative imports
- [Global Dependencies (FastAPI)](https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/) — dependencies that affect everything with `FastAPI(dependencies=[...])`
- [Dependencies in path operation decorators (FastAPI)](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/) — how to place a dependency that doesn't use the return value
- [Dependencies (FastAPI)](https://fastapi.tiangolo.com/tutorial/dependencies/) — the basics of dependency injection with `Depends` and `Annotated`
- [Metadata and Docs URLs (FastAPI)](https://fastapi.tiangolo.com/tutorial/metadata/) — `title`/`version`/`openapi_tags` and control of the docs URLs
- [APIRouter (FastAPI reference)](https://fastapi.tiangolo.com/reference/apirouter/) — all parameters of `APIRouter`
- [FastAPI CLI](https://fastapi.tiangolo.com/fastapi-cli/) — `fastapi dev` / `fastapi run` and startup settings
