Search Topics

Search across all FastAPI topics

GitHub

Project Structure

Fundamentals

Your main.py is 2000 lines long. Your teammate just quit. Let's talk about how to organize a FastAPI project before it turns into a nightmare.

Your FastAPI app is 2000 lines in a single main.py. A teammate needs to add a feature and spends 45 minutes just figuring out where anything lives.

terminal
ImportError: cannot import name 'get_db' from partially initialized module 'main'
(most likely due to a circular import)

Question

Circular imports. The error message that makes you question your life choices. But it's actually a symptom of a bigger problem: everything is in one file, and everything depends on everything else. The fix isn't just "move some imports around" — it's learning how to structure your project so dependencies flow in one direction.

The Transformation

Every growing FastAPI project goes through this journey. The question isn't if you'll need to split things up — it's when.

Single main.py

2000 lines, circular imports

Split by responsibility

routers/, models/, schemas/

Modular structure

Clean, navigable, no conflicts

Why structure matters

What you just learned

A single file works fine for learning — but it doesn't scale

Circular imports happen when files depend on each other in a loop

The fix is organizing code so dependencies flow in one direction

Single File Is Fine (At First)

When you're learning FastAPI, a single main.py is perfectly fine. Don't over-engineer. Keep it simple until complexity demands structure. But once you're past 5-10 endpoints, you'll start feeling the pain.

main.py
# This is totally fine when you're starting out!
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

# But once you have 20 endpoints, 5 models, and 3 services...
# things get messy fast.

The Recommended Structure

As your project grows, organize code by responsibility. Routers handle endpoints, schemas define data shapes, models map to database tables, and config manages settings.

project-layout
my_api/
├── main.py              # App entry point — just wires things together
├── config.py            # Settings & env vars
├── requirements.txt     # Dependencies
├── .env                 # Secrets (gitignored!)
├── routers/
│   ├── __init__.py
│   ├── users.py         # /users endpoints
│   └── items.py         # /items endpoints
├── schemas/
│   ├── __init__.py
│   ├── user.py          # User Pydantic models
│   └── item.py          # Item Pydantic models
├── models/
│   ├── __init__.py
│   └── database.py      # SQLAlchemy models
└── dependencies.py      # Shared dependencies

Insight

Notice the pattern? Each folder has one job. routers/ handles HTTP. schemas/ handles data validation. models/ handles the database. main.py just connects them. If someone asks "where are the user endpoints?" — the answer is always routers/users.py.

Explore: Interactive Project Tree

Click any file or folder to see what belongs inside and why it lives there.

What Goes Where

Each directory has a clear job. Here's the rule of thumb:

  • routers/ — Endpoint definitions grouped by domain (users, items, auth)
  • schemas/ — Pydantic models for request/response validation
  • models/ — Database table definitions (SQLAlchemy, Tortoise, etc.)
  • config.py — Settings and environment variable loading
  • dependencies.py — Shared dependency injection functions

Project layout

What you just learned

Organize by responsibility: routers, schemas, models, config

Each folder answers one question — 'where are the endpoints?' → routers/

main.py becomes the hub that wires everything together

Wiring It Together

Your main.py becomes a thin hub. It creates the app, imports the routers, and mounts them. That's it — no business logic, no models, no schemas.

main.py
from fastapi import FastAPI
from routers import users, items

app = FastAPI(title="My API", version="1.0.0")

# Each router handles its own domain
app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(items.router, prefix="/items", tags=["Items"])

# That's it. main.py is done.
# All the real work happens in the router files.

The circular import trap

You create a router file and import the app instance from main.py. Main.py also imports from the router. Python can't resolve the loop.

Broken code
routers/users.py
# main.py
from fastapi import FastAPI
from routers.users import router  # imports from users.py

app = FastAPI()
app.include_router(router)

# routers/users.py
from main import app  # imports from main.py — CIRCULAR!

@app.get("/users")
async def get_users():
    return []

Think about it...

Your router imports from models, and models imports from database. Where should the DB engine live to avoid circular imports?

Hint: Dependencies should flow in one direction, never form cycles.

Key Points

Start Simple

A single main.py is fine for learning and small projects

Split When It Grows

Organize by responsibility once you have more than a few endpoints

Routers Organize Endpoints

Group related endpoints into separate router files by domain

Schemas Validate Data

Pydantic models in the schemas/ folder define your data contracts

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Circular imports between routers and main
Importing the app instance inside router files
Don't do this
routers/users.py
# routers/users.py
from main import app  # Circular import!

@app.get("/users")
async def get_users():
    return []
Never import the app instance in router files — this creates circular imports. Use APIRouter() in each router file and include_router() in main.py to wire them together.
2
Putting everything in main.py
Defining models, schemas, and dozens of endpoints in one file
Don't do this
main.py
# main.py — 500+ lines with everything mixed together
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String
# ... 20 more imports

app = FastAPI()

class UserDB(Base): ...
class ItemDB(Base): ...
class UserSchema(BaseModel): ...
class ItemSchema(BaseModel): ...

@app.get("/users") ...
@app.post("/users") ...
@app.get("/items") ...
# ... 30 more endpoints
A 500-line main.py becomes impossible to navigate. Split by responsibility: routers for endpoints, schemas for Pydantic models, models for database tables. main.py should just wire things together.