Python FastAPI Tutorial (Part 8): Routers - Organizing Routes into Modules with APIRouter
Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
APIRouter moves resource-specific API endpoints into separate modules while keeping the same external URLs and responses.
Briefing
FastAPI route organization gets a major upgrade by moving API endpoints out of a bloated main.py file and into dedicated modules using APIRouter. The payoff is practical: the external behavior stays identical—same URLs, same responses—while the internal codebase becomes easier to maintain, scale, and test as new features like forms and authentication are added.
The tutorial starts by pointing out how main.py can quickly become crowded: imports, lifespan setup, template routes for the front end, API routes for users and posts, and exception handlers all end up mixed together. Even when everything works, that structure becomes a maintenance risk once CRUD endpoints, async logic, and validation rules multiply. The fix is to split API routes into a routers/ package and register them back into the app.
An APIRouter is introduced as FastAPI’s mechanism for defining routes in separate files. Instead of decorating endpoints on the app object (app.get, app.post), endpoints are decorated on a router instance (router.get, router.post). When the router is included in main.py via app.include_router, a prefix can be applied to all routes in that module, and tags can be used to group endpoints in the automatic documentation.
Implementation follows a clear pattern. A routers directory is created, along with an empty __init__.py to make it a proper Python package. Two router modules are added: routers/users.py and routers/post.py. In each module, only the imports needed for that resource are brought in, reducing unnecessary dependencies in every file.
For route paths, the tutorial uses a key technique: inside each router module, the path strings are written relative to the router’s eventual prefix. For example, user endpoints that originally lived under /api/users are moved into users.py with paths starting as an empty string (''), so that when main.py includes the router with prefix /api/users, the final URL becomes correct. Other endpoints keep their relative segments like /{user_id}. The same approach is applied to posts, using a prefix of /api/posts and relative paths such as '' and /{post_id}.
After moving the endpoints, main.py is updated to import the routers and include them with app.include_router(users.router, prefix='/api/users', tags=['users']) and app.include_router(post.router, prefix='/api/posts', tags=['posts']). The tutorial also flags a common FastAPI gotcha: route function names can conflict with template route names, so endpoints should use unique, descriptive function names across modules.
Verification confirms the refactor didn’t change functionality. The homepage still loads, API endpoints still create users and posts correctly, and the Swagger docs become cleaner—users and posts appear as collapsible groups thanks to tags. With main.py reduced to under 150 lines, the structure now separates concerns cleanly: front-end views remain in main.py, while resource logic lives in routers. The tutorial closes by outlining future organization options (by resource, version, access level, or feature) and previews the next steps: front-end forms using fetch and later authentication rules.
Cornell Notes
APIRouter lets FastAPI developers split large main.py files into resource-focused modules without changing external behavior. Endpoints move from app.get/app.post to router.get/router.post inside routers/users.py and routers/post.py, then get reattached in main.py using app.include_router with a URL prefix like /api/users or /api/posts. Router paths are written relative to the prefix—often using '' for the base route—so the final URLs remain the same. Adding tags in include_router groups endpoints in the Swagger UI under collapsible “users” and “posts” sections. The result is cleaner maintenance, fewer merge conflicts on teams, and easier targeted testing and mocking of dependencies per router.
Why does splitting routes into APIRouter modules matter even when everything already works?
What changes when moving an endpoint from main.py to a router file?
How does using an empty string '' inside a router produce the correct base URL?
How do tags improve FastAPI documentation after routers are added?
What potential naming conflict does FastAPI warn about in this setup?
What practical benefits come from the refactor beyond cleaner code layout?
Review Questions
- When including a router with prefix='/api/users', what should the router’s base endpoint path be if the goal is to map to '/api/users' exactly?
- What two edits are required when moving an endpoint decorator from main.py to an APIRouter module?
- How do tags passed to app.include_router affect the Swagger UI layout?
Key Points
- 1
APIRouter moves resource-specific API endpoints into separate modules while keeping the same external URLs and responses.
- 2
Refactoring main.py into routers/users.py and routers/post.py reduces file clutter and makes endpoint discovery faster.
- 3
Endpoints change from app.get/app.post to router.get/router.post, and their paths become relative to the router’s include_router prefix.
- 4
Using '' as a router base path lets main.py’s prefix (e.g., /api/users) form the correct final route without relying on trailing slashes.
- 5
app.include_router can attach tags to group endpoints in Swagger UI under collapsible sections like “users” and “posts”.
- 6
Unique endpoint function names help avoid FastAPI route-name conflicts with template routes.
- 7
The modular router structure supports scaling, team parallel work, and more targeted testing with mocked dependencies.