Back to Blog
Python

Python FastAPI Routers and Project Structure

python fastapi routers and project structure: Learn how to organize a FastAPI application with routers, structure files for maintainability, and handle dependencies ac...

FastAPIAPIRouterProject OrganizationPython Web DevelopmentAPI Design
Diagram showing a FastAPI application split into modular routers and project folders

When a FastAPI application grows beyond a single file, the way you organize routes and modules becomes the difference between a codebase you can extend and one you have to rewrite. The core tool for this is APIRouter, and the way you combine routers with your application defines your project structure. This article covers the practical mechanics of python fastapi routers and project structure: how to create routers, include them, share dependencies, and lay out files so the app remains readable as it scales.

What APIRouter Provides and Why It Matters

APIRouter is a lightweight class that groups related endpoints under a single namespace. It supports the same decorators as the main FastAPI instance (@router.get, @router.post, etc.) and can define its own prefix, tags, and dependencies. Without routers, every endpoint lives on the main app object, which becomes unwieldy once you have more than a handful of routes. Routers let you separate concerns by resource, feature, or domain, and they make it possible to reuse the same route definitions across multiple applications or test suites.

A router does not execute anything by itself. It is a collection of route definitions that must be included in a FastAPI instance (or another router) to become part of the application. That inclusion step is where the project structure comes into play: you decide which routers exist, how they are grouped, and how they are mounted.

Creating a Router: Minimal Example

Start by defining a router in a dedicated module. Suppose you have a users resource. Create app/routers/users.py:

from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/users", tags=["users"]) @router.get("/") async def list_users(): return [{"id": 1, "name": "Alice"}] @router.get("/{user_id}") async def get_user(user_id: int): if user_id != 1: raise HTTPException(status_code=404, detail="User not found") return {"id": user_id, "name": "Alice"}

The prefix argument prepends /users to every route defined on this router. The tags argument groups these endpoints in the automatically generated OpenAPI documentation. You can also set dependencies at the router level to apply authentication or other checks to all routes in that router.

Notice that the router does not import the FastAPI class. It only imports APIRouter. This keeps the router module independent of the application instance, which is important for avoiding circular imports when the application module imports the router and the router might need something from the application.

Including Routers in the Main Application

The main application file, often app/main.py, creates a FastAPI instance and includes each router using the include_router method:

from fastapi import FastAPI from app.routers import users, products app = FastAPI() app.include_router(users.router) app.include_router(products.router)

When you call include_router, you can override the router's prefix, tags, and dependencies. This is useful when you want to mount the same router under different prefixes or add extra dependencies in a specific deployment. For example, you might include a router with a version prefix:

app.include_router(users.router, prefix="/api/v1")

If the router already has a prefix, the two are concatenated. So a route defined as @router.get("/") with prefix="/users" becomes /api/v1/users/. This flexibility allows you to keep router definitions clean while adapting them to the overall API layout.

Project Structure Patterns for FastAPI

There is no single official FastAPI project layout, but a common pattern that works well for medium and large applications separates the application into layers: routers, schemas, models, and services. A typical structure looks like this:

app/
├── main.py
├── routers/
│   ├── __init__.py
│   ├── users.py
│   └── products.py
├── schemas/
│   ├── __init__.py
│   ├── user.py
│   └── product.py
├── models/
│   ├── __init__.py
│   ├── user.py
│   └── product.py
└── services/
    ├── __init__.py
    ├── user_service.py
    └── product_service.py
  • main.py creates the app and includes routers.
  • routers/ contains APIRouter definitions, each focused on one resource or feature.
  • schemas/ holds Pydantic models for request and response validation.
  • models/ contains database models (e.g., SQLAlchemy) or domain entities.
  • services/ contains business logic that routers call.

This separation keeps routers thin. A router should only handle HTTP concerns: parsing requests, validating input, and returning responses. Business logic belongs in services, and data access belongs in models. When you follow this pattern, you can test routers independently by mocking services, and you can reuse services in other contexts like CLI scripts or background tasks.

For smaller projects, you can flatten the structure and keep everything in a single package. The key is that routers are grouped logically and imported cleanly. Avoid putting all route definitions in main.py because it becomes a bottleneck for collaboration and makes it harder to reason about the application's surface area.

Managing Dependencies Across Routers

Dependencies are a central part of FastAPI's design. They can be defined at the application level, router level, or endpoint level. When you structure your project with routers, you need to decide where each dependency belongs.

A common example is authentication. Suppose you have a function that validates an API key:

from fastapi import Depends, HTTPException, Header def verify_api_key(x_api_key: str = Header(...)): if x_api_key != "secret": raise HTTPException(status_code=401, detail="Invalid API key") return x_api_key

You can apply this dependency to all routes in a router by passing it to the router's dependencies parameter:

router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(verify_api_key)])

Now every route in that router requires the header. If you need the dependency's return value inside a route, you declare it as a parameter with Depends on the individual endpoint, not at the router level. The router-level dependency is only for side effects and access control; it does not inject values into route functions.

Dependencies can also be shared across routers by defining them in a separate module and importing them. This avoids duplication and keeps the logic in one place. For example, a dependencies.py module can hold verify_api_key, get_current_user, and require_admin functions. Routers then import only what they need.

When you have a dependency that applies to the entire application, you can pass it to the FastAPI constructor:

app = FastAPI(dependencies=[Depends(verify_api_key)])

But be careful: this applies to every route, including health checks or public endpoints. It is often better to use router-level dependencies for groups of protected routes and leave the app-level dependencies for truly global concerns like CORS or request ID generation.

Avoiding Circular Imports and Naming Conflicts

A common pitfall when structuring FastAPI projects is circular imports. This happens when the main application imports a router, and that router imports something from the main application (e.g., a dependency or a config). To avoid this, keep routers independent of the FastAPI instance. Routers should only import from modules that do not import the application itself.

For example, if you need a database session in a router, define a dependency function in a separate module (e.g., dependencies.py) that imports the database session factory. The router imports that dependency, not the FastAPI app. The main app imports the router and the dependency module, but the dependency module does not import the app.

Naming conflicts are another issue. If two routers define a route with the same path and method, the last one included wins. This can be subtle because the order of include_router calls determines the final behavior. To prevent this, use distinct prefixes for each router and avoid overlapping paths. If you need to mount the same router under different prefixes, be aware that the route paths are combined, so a router with prefix="/items" and a route @router.get("/{item_id}") becomes /items/{item_id}. If you include it twice with different prefixes, you get two distinct endpoints, which is usually intended.

Production Considerations: Versioning and Testing

In production, the way you structure routers directly affects how you version your API and how you test it. For versioning, you can use the prefix parameter on include_router to mount the same router under /v1 and /v2, but then you need to handle differences between versions. A cleaner approach is to create separate router modules for each version, e.g., routers/v1/users.py and routers/v2/users.py, and include them with distinct prefixes. This keeps version-specific logic separate and avoids conditional code inside a single router.

Testing FastAPI applications benefits from the router structure. You can import a router directly and create a test client for a minimal app that includes only that router, which speeds up tests and isolates failures. For example:

from fastapi.testclient import TestClient from fastapi import FastAPI from app.routers import users app = FastAPI() app.include_router(users.router) client = TestClient(app) def test_list_users(): response = client.get("/users/") assert response.status_code == 200

This pattern lets you test each router in isolation without spinning up the entire application. It also encourages you to keep routers self-contained, which is good for maintainability.

Another production concern is observability. When you have many routers, it helps to add a consistent prefix and tags so that metrics and logs can be grouped by resource. FastAPI's OpenAPI integration already groups by tags, so using meaningful tags on each router improves the generated documentation and makes it easier for consumers to navigate.

Finally, be mindful of the order in which you include routers if you have a catch-all route or middleware that depends on path matching. Since routers are included sequentially, a router with a broad prefix like / could shadow more specific ones if it is included first. Always include specific routers before generic ones, or use distinct prefixes to avoid ambiguity.

python fastapi routers and project structure: Practical Usag | RYUSLOG DEV