Home/Learn/FastAPI/APIRouter — Splitting the App into Modules

APIRouter — Splitting the App into Modules

Intermediate
Routing & Structure

APIRouter lets each domain (students, drives, auth) live in its own file with its own prefix, tags, and dependencies — include_router assembles them into one app with one /docs.

Overview

A real service never stays in one main.py. APIRouter is a mini-FastAPI: declare routes on it exactly as on the app, then app.include_router() mounts it — with a shared prefix ("/students"), tags for the docs, and even router-wide dependencies like authentication. The standard layout that interviewers expect you to sketch has routers/ for HTTP handling, schemas/ for Pydantic models, services/ for business logic, and models/ for the ORM — routers stay thin, logic stays testable without HTTP. Versioning falls out naturally: mount the same routers under /api/v1 and keep /api/v2 free for the future.

Routers In, One App Out

Each file owns a domain. The router declares prefix and tags once; every route in the file inherits them. main.py becomes an assembly point that never grows.

One domain per file; main.py just includes routers
# ── app/routers/students.py ─────────────────
from fastapi import APIRouter, HTTPException

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

@router.get("")                          # → GET /students
def list_students():
    return [{"id": 1, "name": "Asha"}]

@router.get("/{student_id}")             # → GET /students/42
def get_student(student_id: int):
    if student_id != 1:
        raise HTTPException(404, "student not found")
    return {"id": 1, "name": "Asha"}

# ── app/routers/drives.py ───────────────────
from fastapi import APIRouter

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

@router.post("", status_code=201)
def create_drive(payload: dict):
    return payload

# ── app/main.py — assembly only ─────────────
from fastapi import FastAPI
from app.routers import students, drives

app = FastAPI(title="Placement API")
app.include_router(students.router)
app.include_router(drives.router)

# Versioning: mount everything under /api/v1 in one line
# app.include_router(students.router, prefix="/api/v1")
# /docs shows Students and Drives as separate sections automatically

The Standard Layout — Thin Routers, Fat Services

Routers translate HTTP ↔ domain; services hold the business rules and know nothing about HTTP. This split is what makes logic unit-testable and the codebase navigable — and it is the structure hiring managers probe for.

routers/schemas/services/models — the sketch to draw in interviews
# app/
# ├── main.py               ← create app, include routers, middleware
# ├── routers/              ← HTTP layer: parse request, call service, shape response
# │   ├── students.py
# │   └── drives.py
# ├── schemas/              ← Pydantic: StudentIn, StudentOut, DriveIn...
# │   └── student.py
# ├── services/             ← business logic: pure Python, no HTTP imports
# │   └── student_service.py
# ├── models/               ← SQLAlchemy ORM classes (Databases chapters)
# │   └── student.py
# └── core/                 ← settings, security, db session

# ── services/student_service.py — no FastAPI anywhere ──
def eligible_for_drive(student, drive) -> bool:
    return (student.cgpa >= drive.min_cgpa
            and student.backlogs == 0
            and student.branch in drive.branches)

# ── routers/students.py — thin: HTTP in, HTTP out ──
from fastapi import APIRouter, HTTPException
from app.services import student_service

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

@router.get("/{student_id}/eligibility/{drive_id}")
def check(student_id: int, drive_id: int):
    student, drive = load_both_or_none(student_id, drive_id)   # repo/db layer
    if not student or not drive:
        raise HTTPException(404, "student or drive not found")
    return {"eligible": student_service.eligible_for_drive(student, drive)}

# Why it matters: eligible_for_drive() is testable with plain pytest —
# no TestClient, no DB, no HTTP. The router test then only checks wiring.

Key Points to Remember

  • 1APIRouter(prefix, tags) per domain file; app.include_router() assembles
  • 2Prefix/tags/dependencies declared once apply to every route in the router
  • 3Thin routers (HTTP translation) + fat services (business logic, no HTTP)
  • 4Version APIs by mounting routers under /api/v1 prefixes

Interview Questions

Sign in to ask Aria
1

Structure a FastAPI project for a food-delivery backend — draw the folders and what lives where.

MediumSwiggy
2

Why should business logic not live inside path-operation functions?

MediumFreshworks
3

How would you serve /api/v1 and /api/v2 simultaneously during a migration?

HardRazorpay

Ask Aria about APIRouter — Splitting the App into Modules

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…