Skip to content

Modularizing Your FastAPI AppΒΆ

🎯 What You’ll LearnΒΆ

  • Why modularization matters in real-world APIs
  • How to use APIRouter to split your app into logical components
  • How to organize your FastAPI project into files and folders
  • How to register routers in the main app

🧠 Why Modularize?¢

As your app grows, a single main.py file becomes unmanageable. Modularization helps you:

  • Separate concerns (e.g., users, tasks, auth)
  • Reuse logic across endpoints
  • Improve readability and testability
  • Scale to production-grade architecture

βœ‹ Before we beginΒΆ

We will use some HTTP response status codes in the following sections. If you are not familiar with them, you can find a comprehensive list of status codes with their explanations on MDN: HTTP response status codes - HTTP | MDN


🧩 Step 1: Understand APIRouter¢

FastAPI provides APIRouter to define routes in isolated modules. Each router behaves like a mini FastAPI app that can be mounted into the main app.


πŸ—‚οΈ Step 2: Restructure Your ProjectΒΆ

Let’s refactor the Task Manager API into a modular layout:

task_manager/
β”œβ”€β”€ main.py
β”œβ”€β”€ models/
β”‚   └── task.py
β”œβ”€β”€ routers/
β”‚   └── tasks.py
└── __init__.py

πŸ“¦ Step 3: Define the Task ModelΒΆ

πŸ“„ models/task.py

from pydantic import BaseModel
from typing import Optional

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    completed: bool = False

πŸ”€ Step 4: Create the Task RouterΒΆ

πŸ“„ routers/tasks.py

from fastapi import APIRouter, HTTPException
from models.task import Task

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

# In-memory store
tasks = {}
task_id_counter = 1

@router.post("/", status_code=201)
def create_task(task: Task):
    global task_id_counter
    task_data = task.model_dump()
    task_data["id"] = task_id_counter
    tasks[task_id_counter] = task_data
    task_id_counter += 1
    return task_data

@router.get("/")
def get_all_tasks():
    return list(tasks.values())

@router.get("/{task_id}")
def get_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]

@router.put("/{task_id}")
def update_task(task_id: int, updated_task: Task):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    task_data = updated_task.model_dump()
    task_data["id"] = task_id
    tasks[task_id] = task_data
    return task_data

@router.delete("/{task_id}", status_code=204)
def delete_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks[task_id]
  • prefix="/tasks" means all routes start with /tasks
  • tags=["Tasks"] groups them in Swagger UI

πŸš€ Step 5: Register the RouterΒΆ

πŸ“„ main.py

from fastapi import FastAPI
from routers import tasks

app = FastAPI()

app.include_router(tasks.router)
  • include_router() mounts the task router into the main app
  • Now all task routes are available under /tasks

πŸ“š Step 6: Swagger UI Still WorksΒΆ

Visit http://127.0.0.1:8000/docs and you’ll see:

  • All your task routes grouped under the β€œTasks” tag
  • Full request/response schemas
  • Interactive testing still works

🧠 Recap¢

You now have a modular FastAPI app:

  • Models live in models/
  • Routes live in routers/
  • The main app just wires everything together

This structure is scalable, testable, and production-ready.


πŸ§ͺ Practice ChallengeΒΆ

Add a new router:

  • Create routers/ping.py with a single GET /ping route that returns {"pong": true}
  • Register it in main.py
  • Confirm it appears in Swagger UI under a new tag