Skip to main content

FastAPI Route Organization

As your FastAPI application grows, managing all your endpoints in a single file becomes challenging. In this tutorial, we'll explore how to organize your FastAPI routes into logical groups using various techniques that help maintain a clean, scalable codebase.

Why Organize Routes?

Before diving into the implementation details, let's understand why organizing routes is important:

  1. Maintainability: Smaller, focused files are easier to maintain
  2. Reusability: Well-organized code is easier to reuse across projects
  3. Collaboration: Team members can work on different parts of the API simultaneously
  4. Scalability: A modular structure makes it easier to add new features
  5. Testing: Isolated components are easier to test

Approach 1: Using APIRouter

FastAPI provides a powerful tool called APIRouter that helps organize endpoints into groups. Think of it as a mini-FastAPI application that can be included in your main app.

Basic APIRouter Example

Let's start with a simple example:

python
from fastapi import APIRouter, FastAPI

app = FastAPI()

# Create an APIRouter instance
router = APIRouter()

# Define routes on the router
@router.get("/items/")
async def read_items():
return [{"name": "Item 1"}, {"name": "Item 2"}]

@router.post("/items/")
async def create_item(name: str):
return {"name": name}

# Include the router in the main app
app.include_router(router)

This simple example doesn't show the full power of APIRouter. Let's see how we can organize routes across multiple files.

Approach 2: File-Based Organization

A common pattern is to organize routes into separate files based on their functionality.

Project Structure

project/
├── main.py
├── routers/
│ ├── __init__.py
│ ├── items.py
│ ├── users.py
│ └── orders.py

Implementation

routers/items.py:

python
from fastapi import APIRouter, HTTPException

router = APIRouter()

@router.get("/")
async def read_items():
return [{"name": "Item 1"}, {"name": "Item 2"}]

@router.get("/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id, "name": f"Item {item_id}"}

@router.post("/")
async def create_item(name: str):
return {"name": name}

routers/users.py:

python
from fastapi import APIRouter, HTTPException

router = APIRouter()

@router.get("/")
async def read_users():
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

@router.get("/{user_id}")
async def read_user(user_id: int):
return {"id": user_id, "name": f"User {user_id}"}

main.py:

python
from fastapi import FastAPI
from routers import items, users

app = FastAPI()

# Include routers with prefixes
app.include_router(
items.router,
prefix="/items",
tags=["items"],
)

app.include_router(
users.router,
prefix="/users",
tags=["users"],
)

@app.get("/")
async def root():
return {"message": "Welcome to the API"}

What's happening here?

  1. Each module (items.py, users.py) defines its own router with routes
  2. The main application includes these routers with specific prefixes
  3. The tags parameter helps organize endpoints in the automatic documentation

Approach 3: Using Prefixes, Tags and Dependencies

You can enhance your route organization with additional parameters:

python
from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()

async def verify_token(x_token: str = Header(...)):
if x_token != "valid-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")

async def verify_admin(x_admin_id: str = Header(...)):
if x_admin_id != "admin-123":
raise HTTPException(status_code=400, detail="X-Admin-Id header invalid")
return x_admin_id

from routers import items, users, admin

# Regular user routes
app.include_router(
items.router,
prefix="/items",
tags=["items"],
dependencies=[Depends(verify_token)],
responses={404: {"description": "Not found"}},
)

# Admin routes with additional dependency
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(verify_token), Depends(verify_admin)],
responses={418: {"description": "I'm a teapot"}},
)

In this example:

  • All routes under /items require a valid token
  • All routes under /admin require both a valid token and admin privileges
  • We've added custom responses to document possible error codes

Approach 4: Creating Logical API Versions

As your API evolves, you might need to support multiple versions. APIRouter can help with this:

python
from fastapi import FastAPI
from routers.v1 import items as items_v1
from routers.v2 import items as items_v2

app = FastAPI()

app.include_router(
items_v1.router,
prefix="/v1/items",
tags=["items", "v1"],
)

app.include_router(
items_v2.router,
prefix="/v2/items",
tags=["items", "v2"],
)

This approach lets you maintain backward compatibility while introducing new features.

Real-World Example: E-Commerce API

Let's see how we might organize a more complete e-commerce API:

ecommerce_api/
├── main.py
├── dependencies.py
├── database.py
├── config.py
├── routers/
│ ├── __init__.py
│ ├── products.py
│ ├── categories.py
│ ├── users.py
│ ├── orders.py
│ └── payments.py
├── models/
│ ├── __init__.py
│ ├── product.py
│ ├── user.py
│ └── order.py
└── schemas/
├── __init__.py
├── product.py
├── user.py
└── order.py

dependencies.py:

python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)):
# Validate token and return user
user = get_user_from_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user

async def get_admin_user(current_user = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized"
)
return current_user

routers/products.py:

python
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from .. import schemas, models, dependencies
from ..database import get_db
from sqlalchemy.orm import Session

router = APIRouter()

@router.get("/", response_model=List[schemas.Product])
async def read_products(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
products = db.query(models.Product).offset(skip).limit(limit).all()
return products

@router.post("/", response_model=schemas.Product, status_code=status.HTTP_201_CREATED)
async def create_product(
product: schemas.ProductCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(dependencies.get_admin_user)
):
db_product = models.Product(**product.dict(), created_by_id=current_user.id)
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product

main.py:

python
from fastapi import FastAPI
from .routers import products, categories, users, orders, payments
from .config import settings

app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION
)

# Include all routers
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(products.router, prefix="/products", tags=["products"])
app.include_router(categories.router, prefix="/categories", tags=["categories"])
app.include_router(orders.router, prefix="/orders", tags=["orders"])
app.include_router(payments.router, prefix="/payments", tags=["payments"])

@app.get("/")
async def root():
return {"message": "Welcome to the E-Commerce API"}

This structure provides:

  • Clean separation of concerns
  • Easy maintenance
  • Organized API documentation (grouped by tags)
  • Centralized authentication and authorization

Best Practices for Route Organization

  1. Group by Domain: Organize routes based on business domains (users, products, etc.)
  2. Consistent Naming: Use consistent naming conventions across your project
  3. Centralize Dependencies: Define common dependencies in a central module
  4. Use Tags: Tags improve API documentation organization
  5. Standardize Responses: Create consistent response structures
  6. Version Your API: Plan for versioning from the start
  7. Separate Concerns: Keep route definitions separate from business logic
  8. Limit Router Size: If a router file becomes too large, consider breaking it down further

Summary

Proper route organization is essential for building maintainable FastAPI applications. By using APIRouter and following good practices for file structure, you can create clean, scalable APIs that are easy to test and extend.

Key techniques include:

  • Using APIRouter to organize endpoints
  • Organizing routes into separate files/modules
  • Applying prefixes, tags, and dependencies
  • Implementing versioning strategies

With these approaches, you'll be well-equipped to build and maintain FastAPI applications of any size.

Additional Resources

Exercises

  1. Convert a single-file FastAPI application into one that uses multiple routers
  2. Implement an authentication system using dependencies and route organization
  3. Create a simple blog API with separate routers for posts, users, and comments
  4. Design and implement a versioned API with both v1 and v2 endpoints
  5. Add comprehensive documentation tags to improve your API's auto-generated docs


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)