As endpoints pile up, main.py becomes impossible to manage. APIRouter lets you group related paths, move them into modules, and reduce merge conflicts. Building on the user and task APIs created so far, this part walks through splitting files with APIRouter and keeping imports clean.
# Before (everything inside main.py)
app = FastAPI()
@app.post("/tasks")
def create_task(...):
...
@app.get("/tasks")
def list_tasks():
...
# After (lean main.py that just mounts routers)
app.include_router(tasks.router)
app.include_router(users.router)
Slimming down the entrypoint makes it obvious where new features live and keeps diffs isolated per team.
Key terms
- APIRouter: Groups related routes into a reusable module. It is the star of this article.
- include_router: Combines separate routers into a single FastAPI app.
- prefix: A string automatically added to every path (for example
/tasks) so you do not repeat yourself. - tags: Labels that show up in Swagger UI, making features easy to find when routers are separated.
- Dependencies: Router-level injections, such as authentication guards, that apply to every endpoint within that router.
Practice card
- Estimated time: 40 minutes
- Prereqs: Part 6 project structure, basic Python packages
- Goal: Create an
APIRouter, then attach it withinclude_router
Core ideas
- Create an
app/apifolder and router module - Register paths on
APIRouterand mount it on the main app - Declare shared prefixes and tags to tidy the docs
Folder layout example
my-fastapi-app/
├─ app/
│ ├─ __init__.py
│ ├─ main.py
│ └─ api/
│ ├─ __init__.py
│ └─ tasks.py
└─ pyproject.toml
app/main.py now focuses on creating the FastAPI instance and registering routers. The __init__.py files can stay empty—they simply tell Python to treat the folders as packages.
Define a router
app/api/tasks.py:
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/tasks", tags=["tasks"])
class Task(BaseModel):
title: str
done: bool = False
tasks: list[dict] = []
current_id = 0
@router.post("", status_code=201)
def create_task(task: Task):
global current_id
current_id += 1
record = {"id": current_id, **task.model_dump()}
tasks.append(record)
return record
@router.get("")
def list_tasks():
return tasks
@router.get("/{task_id}")
def get_task(task_id: int):
for t in tasks:
if t["id"] == task_id:
return t
raise HTTPException(status_code=404, detail="Task not found")
Thanks to prefix="/tasks", you avoid repeating the base path, and Swagger UI places everything under the tasks tag.
⚠️ We keep using an in-memory list to stay consistent with earlier parts. Production apps should persist data via a database or service so the state does not disappear on restart.
Wire it up in the main file
app/main.py:
from fastapi import FastAPI
from app.api import tasks # app/api/__init__.py re-exports the module
app = FastAPI()
app.include_router(tasks.router)
@app.get("/health")
def health_check():
return {"status": "ok"}
Call include_router as many times as needed inside main.py before the app starts serving traffic. You can add router-level dependencies or responses to apply shared behavior.
Practical example: admin router (preview)
Once a single router feels comfortable, you can register multiple routers with different prefixes, tags, and dependency guards.
from fastapi import Depends
from app.api import tasks, admin
def check_admin(user = Depends(get_current_user)):
if not user.is_admin:
raise HTTPException(status_code=403)
return user
app.include_router(tasks.router)
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(check_admin)],
)
dependencies=[Depends(check_admin)] applies the guard to every route inside the admin router. We cover dependency injection in depth in Part 8, so treat this snippet as a preview rather than required reading.
uv commands and import paths
uv (the Python project runner) still launches the app the same way—just point to the reorganized module path: uv run fastapi dev app/main.py. When running Uvicorn directly, use uv run uvicorn app.main:app --reload, and run both commands from the project root so imports resolve.
Things to watch when splitting
- Circular imports: if
tasks.pyimportsusers.pyandusers.pyimportstasks.py, Python raisesImportError. Fix this by moving shared schemas intoapp/schemas.pyand importing from there. - Shared config: create packages such as
core,schemas, orservicesonce routes need settings, database helpers, or business logic. Routers should stay thin. - Docs navigation: set meaningful
tagsso/docsremains scannable even when dozens of routers exist.
Practice
- Follow along: Build
app/api/tasks.pywithAPIRouterand register it fromapp/main.py. - Extend: Add an admin router with its own prefix and tag, then confirm
/docsshows separate sections. - Debug: Fix execution errors caused by import loops or wrong module paths, or use the tip below.
- Done when: Running
uv run fastapi dev app/main.pyshows multiple tags in/docs. - Troubleshoot: If you see
ModuleNotFoundError: No module named 'app', run the command from the repository root so Python can find the package.
Wrap-up
APIRouter is the baseline tool for keeping FastAPI tidy. Splitting files plus declaring prefixes and tags instantly clarifies the structure. Next we will use the current codebase to prepare for testing and deployment flows.
💬 댓글
이 글에 대한 의견을 남겨주세요