FastAPI makes it easy to get started with APIs.
But when you're thinking about building real projects — projects that might one day be used by thousands of users — structure and scalability matter from day one.
In this post, I'll show you:
- How to structure your FastAPI project cleanly
- How to organize versioned routes (so you can evolve your API over time)
- Why using Docker even for small apps saves you headaches later
- Clear explanations for every design decision (not just code)
📦 Why Structure Matters
When you first start, it’s tempting to put everything in a main.py
.
And that’s fine — at first.
But as soon as your app grows:
- It becomes hard to maintain
- You can’t version your API easily
- Adding new features becomes a mess
That’s why we’ll set up a modular, versioned, dockerized FastAPI app — from the start.
🏗 Project Structure (Professional Layout)
fastapi-app/
├── app/
│ ├── api/
│ │ ├── v1/
│ │ │ ├── endpoints/
│ │ │ │ ├── users.py
│ │ │ │ └── items.py
│ │ │ └── __init__.py
│ │ └── __init__.py
│ ├── core/
│ │ ├── config.py
│ │ └── __init__.py
│ ├── main.py
│ └── __init__.py
├── Dockerfile
├── requirements.txt
└── README.md
Why this structure?
- app/api/v1/: all your routes go here, grouped by version (easy to maintain APIs even if your app lives 5+ years)
-
endpoints/: split your functionality logically —
users.py
for user routes,items.py
for item routes, and so on. - core/: keep your app-wide configuration (env vars, settings) separate.
- main.py: the real entry point — it brings everything together cleanly.
Good structure = easier scaling, faster onboarding of new developers, and fewer bugs over time.
🐍 Building the FastAPI App
requirements.txt
fastapi
uvicorn
We keep dependencies lightweight to start. Later, you’ll add things like SQLAlchemy, Alembic, etc.
app/core/config.py
class Settings:
PROJECT_NAME: str = "FastAPI App"
API_V1_STR: str = "/api/v1"
settings = Settings()
👉 Why?
Instead of hardcoding paths like /api/v1/
everywhere, we centralize configuration in one place.
Later, you can add database URLs, secrets, CORS configs here too.
app/api/v1/endpoints/users.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/users", tags=["users"])
def get_users():
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
👉 Why use an APIRouter?
- Routers keep related endpoints grouped together
- Easier to version, test, and maintain in the long run
- You can apply middlewares, auth, and permissions per-router later
app/api/v1/endpoints/items.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/items", tags=["items"])
def get_items():
return [{"id": "book", "price": 12}, {"id": "laptop", "price": 999}]
app/api/v1/__init__.py
from fastapi import APIRouter
from app.api.v1.endpoints import users, items
router = APIRouter()
router.include_router(users.router)
router.include_router(items.router)
👉 Why a central router?
Instead of cluttering main.py
, we import and group all v1 routes here neatly.
app/main.py
from fastapi import FastAPI
from app.core.config import settings
from app.api.v1 import router as v1_router
app = FastAPI(title=settings.PROJECT_NAME)
app.include_router(v1_router, prefix=settings.API_V1_STR)
@app.get("/")
def read_root():
return {"message": "Welcome to the FastAPI backend!"}
👉 Key points:
- The root path gives a basic welcome message.
- We attach all versioned routes under
/api/v1
, easily expandable to/api/v2
someday without chaos.
🐳 Why Docker?
Even if you're just starting, Docker brings 3 huge benefits:
Benefit | Why It Matters |
---|---|
Consistency | Same environment everywhere (local, staging, production) |
Isolation | No \"works on my machine\" problems |
Deployment Ready | You're always 1 docker build away from deploying |
📄 Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app
COPY app ./app
# Run app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
Why this Dockerfile?
- Python 3.11 slim image = fast and minimal
-
Workdir
/app
= standard for small backend services - No-cache pip install = smaller image sizes
- Uvicorn reload mode = fast development cycles
🏃♂️ Running the App
Local dev:
pip install -r requirements.txt
uvicorn app.main:app --reload
Using Docker:
docker build -t fastapi-app .
docker run -p 8000:8000 fastapi-app
Access your app:
http://localhost:8000/
-
http://localhost:8000/docs
(beautiful auto Swagger UI)
🧠 Why Versioning Early?
Imagine:
-
/api/v1/
is live with 10,000 users - You want to change your API behavior — without breaking old clients
With versioning:
- You simply create
/api/v2/
- Clients who want the new features switch to v2
- Older apps can keep using v1 safely
This is how real companies (Stripe, Twilio, etc.) handle their APIs.
🏁 Final Thoughts
✅ Structured project = easier scaling
✅ Versioned APIs = future-proofing your backend
✅ Docker = production readiness from day one
FastAPI gives you the speed of development — but your structure is what keeps your project alive long-term.
🚀 What's Next?
If you enjoyed this setup, in the next post I'll show you how to:
- Add JWT Authentication
- Connect to a real PostgreSQL Database
- Write tests to make your app production-grade
Stay tuned! 🎯