FastAPI is one of the best Python web frameworks available today — fast, async-native, and backed by excellent tooling. But when you move beyond a single main.py file, you quickly realize that FastAPI gives you the engine, not the car. Structuring a production-ready application is entirely up to you.
Let's walk through what that looks like in practice.
Starting from Scratch
1. Project Layout
A typical "real" FastAPI project ends up looking something like this:
my_app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── logger.py
│ ├── dependencies.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── posts.py
│ └── models/
│ ├── __init__.py
│ ├── user.py
│ └── post.py
├── tests/
├── .env
├── .env.production
├── pyproject.toml
└── README.md
Already a lot of scaffolding — and we haven't written a single route yet.
2. Loading Environment Variables
FastAPI has no built-in env loading. You reach for python-dotenv:
pip install python-dotenv
# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
env = os.environ.get("APP_ENV", "production")
if env != "production":
load_dotenv(f".env.{env}", override=True)
DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
But now every config value is a raw string. You need to cast types yourself, handle missing keys yourself, and figure out how to share this across modules without circular imports.
Some teams reach for Pydantic's BaseSettings:
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
redis_host: str = "localhost"
redis_port: int = 6379
class Config:
env_file = ".env"
settings = Settings()
Better — but now you have two config systems if you need per-environment overrides, and no clean way to namespace configs (database.host vs cache.host).
3. Setting Up the Database
pip install sqlalchemy asyncpg alembic
# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine(settings.database_url, echo=settings.debug)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
class Base(DeclarativeBase):
pass
async def get_db():
async with AsyncSessionLocal() as session:
yield session
Then you wire that into every route as a dependency:
# app/routers/users.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
router = APIRouter()
@router.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User))
return result.scalars().all()
Every route needs that Depends(get_db). Every model needs to know about Base. Alembic needs its own env.py wired to your engine. Migrations are a separate manual step to configure.
4. Configuring Logging
FastAPI has no built-in logging configuration. There's no framework opinion — you wire it yourself:
# app/logger.py
import logging
import sys
def configure_logging(debug: bool = False):
level = logging.DEBUG if debug else logging.INFO
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(
"%(asctime)s — %(name)s — %(levelname)s — %(message)s"
))
root = logging.getLogger()
root.setLevel(level)
root.addHandler(handler)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
Then call it at startup:
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.logger import configure_logging
from app.config import settings
from app.routers import users, posts
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_logging(debug=settings.debug)
yield
app = FastAPI(lifespan=lifespan)
app.include_router(users.router, prefix="/api")
app.include_router(posts.router, prefix="/api")
5. Dependency Injection — Roll Your Own
FastAPI's Depends() system is powerful but low-level. Need to inject a service across dozens of routes? You'll write a factory function, register it, and thread it through every handler manually. There's no service container. No provider pattern. No automatic resolution.
6. CLI / Management Commands
Need to run migrations? Seed the database? Clear a cache? FastAPI has no CLI. You'll integrate Alembic, write custom Click commands, wire them to a Makefile or shell scripts, and document everything yourself.
The Honest Summary
By the time you've wired up environment loading, database connections, migrations, logging, dependency injection, CLI commands, and a sensible folder structure — you've built a mini-framework on top of FastAPI.
And you'll do it again for the next project. And the one after that.
There's a Better Way
FastAPI Startkit is a Laravel/Masonite-inspired framework that replaces all of that manual wiring. Let's rebuild the same app — one layer at a time.
Step 1 — A running FastAPI app in one file
Install it:
uv add fastapi-startkit[fastapi]
That's the only dependency you need to start. Here's the minimal main.py:
from pathlib import Path
from fastapi_startkit import Application
from fastapi_startkit.fastapi import FastAPIProvider
app: Application = Application(
base_path=Path(__file__),
providers=[
FastAPIProvider,
]
)
fastapi = app.fastapi
Run it:
uvicorn main:fastapi --reload
Your app is live. No lifespan, no manual FastAPI() instantiation, no setup boilerplate.
Step 2 — Add routes
app.fastapi is a standard FastAPI instance — use FastAPI's own APIRouter exactly as you already know:
from pathlib import Path
+ from fastapi import APIRouter
from fastapi_startkit import Application
from fastapi_startkit.fastapi import FastAPIProvider
app: Application = Application(
base_path=Path(__file__),
providers=[
FastAPIProvider,
]
)
fastapi = app.fastapi
+ router = APIRouter()
+
+ @router.get("/users")
+ async def list_users():
+ return [{"id": 1, "name": "Alice"}]
+
+ @router.get("/users/{user_id}")
+ async def show_user(user_id: int):
+ return {"id": user_id, "name": "Alice"}
+
+ fastapi.include_router(router)
No new API to learn. It's just FastAPI.
Step 3 — Add logging
Swap the standard logging setup for LogProvider. One line:
from pathlib import Path
from fastapi_startkit import Application
from fastapi_startkit.fastapi import FastAPIProvider
+ from fastapi_startkit.logging import LogProvider
app: Application = Application(
base_path=Path(__file__),
providers=[
+ LogProvider,
FastAPIProvider,
]
)
Logging is now configured and active. Change the level with an env var — no code change required:
LOG_LEVEL=DEBUG uvicorn main:fastapi --reload
Step 4 — Add the database
from pathlib import Path
from fastapi_startkit import Application
from fastapi_startkit.fastapi import FastAPIProvider
from fastapi_startkit.logging import LogProvider
+ from fastapi_startkit.masoniteorm import DatabaseProvider
app: Application = Application(
base_path=Path(__file__),
providers=[
LogProvider,
+ DatabaseProvider,
FastAPIProvider,
]
)
Define a model — no Base, no DeclarativeBase, no session factory:
from fastapi_startkit.masoniteorm import Model
class User(Model):
__table__ = "users"
id: int
name: str
email: str
Query it anywhere:
users = await User.all()
user = await User.find(1)
await User.create({"name": "Alice", "email": "alice@example.com"})
Run migrations from the terminal:
uv run artisan migrate
uv run artisan make:model Post
uv run artisan make:migration create_posts_table
No Alembic. No custom env.py. No session dependency threaded through every route.
The full picture
Starting from nothing, here's the complete progression:
from pathlib import Path
from fastapi_startkit import Application
+ from fastapi_startkit.logging import LogProvider
+ from fastapi_startkit.masoniteorm import DatabaseProvider
from fastapi_startkit.fastapi import FastAPIProvider
app: Application = Application(
base_path=Path(__file__),
providers=[
+ LogProvider,
+ DatabaseProvider,
FastAPIProvider,
]
)
fastapi = app.fastapi
Three lines added. Logging, database, and a fully wired FastAPI instance — all ready.
Conclusion
FastAPI is an excellent foundation. But building around it — environment management, configuration, ORM, logging, CLI, dependency injection — takes real effort and tends to drift between projects.
FastAPI Startkit gives you that structure from day one, so you can focus on shipping features instead of plumbing.












