When building an API with FastAPI, it’s deceptively easy to dump every endpoint into main.py. A few @app.get() decorators here, a couple of @app.post() decorators there, and everything works. Then your project grows. Suddenly, main.py is an 800-line monolith, team members are constantly hitting Git merge conflicts, and nobody knows where the user authentication logic ends and the billing logic begins. FastAPI solves this with APIRouter. Here is how to structure your routing cleanly from day one. 1. The Core Tool: APIRouter Think of APIRouter as a mini FastAPI app that doesn’t run on its own. It groups related path operations together, which you then mount onto your main application. A typical modular directory structure looks like this: my_project/ ├── app/ │ ├── routers/ │ │ ├── users.py │ │ └── items.py │ ├── dependencies.py │ └── main.py └── requirements.txt In your module (app/routers/users.py), define your router: from fastapi import APIRouter, HTTPException router = APIRouter() @router.get("/") async def list_users(): return [{"id": 1, "username": "alice"}] @router.get("/{user_id}") async def get_user(user_id: int): return {"id": user_id, "username": "alice"} Then, register it in app/main.py: from fastapi import FastAPI from app.routers import users app = FastAPI() app.include_router(users.router, prefix="/users", tags=["Users"]) Two key parameters to notice: prefix: Prepend /users to every endpoint in the router. Notice how users.py uses / and /{user_id} instead of repeating /users everywhere. tags: Groups these endpoints together in FastAPI's auto-generated Swagger UI (/docs). 2. Cleaner Endpoints with Shared Dependencies One of the biggest advantages of APIRouter is attaching security checks or common dependencies to an entire group of routes at once. Instead of writing Depends(verify_token) on 15 separate endpoints, apply it to the router level: # app/routers/admin.py from fastapi import APIRouter, Depends from app.dependencies import require_admin_role # Every route attached to this router will now require admin access router = APIRouter( prefix="/admin", tags=["Admin"], dependencies=[Depends(require_admin_role)] ) @router.delete("/purge-cache") async def purge_cache(): return {"status": "cache cleared"} 3. Nesting Routers for Sub-Resources Real-world APIs often have hierarchical relationships—such as comments belonging to a specific post (/posts/{post_id}/comments). Rather than hardcoding long prefix strings, routers can be nested inside other routers: # app/routers/comments.py from fastapi import APIRouter comments_router = APIRouter() @comments_router.get("/") async def get_comments(post_id: int): return {"post_id": post_id, "comments": []} # app/routers/posts.py from fastapi import APIRouter from app.routers.comments import comments_router posts_router = APIRouter(prefix="/posts", tags=["Posts"]) # Mount comments directly under a specific post posts_router.include_router( comments_router, prefix="/{post_id}/comments", tags=["Comments"] ) Now, mounting posts_router inside your main.py automatically exposes both /posts and /posts/{post_id}/comments cleanly. 4. API Versioning Without Headaches Versioning is simple when your routing is modular. You can bundle whole sets of routers under a version prefix: # app/main.py from fastapi import FastAPI, APIRouter from app.routers import users, items app = FastAPI() # Create a v1 router api_v1 = APIRouter(prefix="/api/v1") api_v1.include_router(users.router, prefix="/users", tags=["Users"]) api_v1.include_router(items.router, prefix="/items", tags=["Items"]) # Include the full v1 bundle into the main app app.include_router(api_v1) When it's time for v2, you can spin up an api_v2 router alongside it without touching older client integrations.