DEV Community

Aldorax
Aldorax

Posted on

πŸš€ How to Build a Scalable FastAPI Backend with Docker and Versioned APIs β€” The Right Way (Opinionated)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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"}]
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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}]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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!"}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Using Docker:

docker build -t fastapi-app .
docker run -p 8000:8000 fastapi-app
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)