Search Topics
Search across all FastAPI topics
Project Structure
FundamentalsYour 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.
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.
# 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.
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 dependenciesInsight
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.
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.
# 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.
# routers/users.py
from main import app # Circular import!
@app.get("/users")
async def get_users():
return []# 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