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! ๐ŸŽฏ