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! 🎯