Cheatsheet for wiring the four libraries together.
Layout
myapp/
├── pyproject.toml
├── alembic.ini
├── migrations/
│ ├── env.py
│ └── versions/
├── src/myapp/
│ ├── __init__.py
│ ├── main.py # FastAPI app + lifespan
│ ├── settings.py # pydantic-settings
│ ├── db.py # engine + sessionmaker + Base
│ ├── models.py # SQLAlchemy
│ ├── schemas.py # Pydantic
│ ├── deps.py # Depends
│ ├── api/
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── auth.py
│ └── services/
│ └── user_service.py
└── tests/
pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"sqlalchemy>=2.0",
"asyncpg>=0.30",
"alembic>=1.14",
"pydantic[email]>=2.9",
"pydantic-settings>=2.6",
"structlog>=24",
]
[dependency-groups]
dev = ["pytest", "pytest-anyio", "httpx", "pytest-cov", "testcontainers[postgres]", "ruff", "mypy"]
settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr
class Settings(BaseSettings):
env: str = "dev"
database_url: str
secret_key: SecretStr
log_level: str = "INFO"
model_config = SettingsConfigDict(env_file=".env", env_prefix="MYAPP_")
from functools import lru_cache
@lru_cache
def get_settings() -> Settings:
return Settings()
db.py
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=NAMING_CONVENTION)
models.py
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import DateTime, func
from datetime import datetime
from .db import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True, index=True)
hashed_password: Mapped[str]
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
schemas.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserRead(UserBase):
id: int
created_at: datetime
model_config = {"from_attributes": True}
main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from .settings import get_settings
from .api import users
@asynccontextmanager
async def lifespan(app: FastAPI):
s = get_settings()
engine = create_async_engine(
s.database_url,
pool_size=20, max_overflow=10,
pool_pre_ping=True, pool_recycle=3600,
)
app.state.engine = engine
app.state.sm = async_sessionmaker(engine, expire_on_commit=False)
yield
await engine.dispose()
app = FastAPI(lifespan=lifespan)
app.include_router(users.router)
deps.py
from fastapi import Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db(request: Request) -> AsyncSession:
async with request.app.state.sm() as session:
yield session
api/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..deps import get_db
from ..models import User
from ..schemas import UserCreate, UserRead
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/", response_model=UserRead, status_code=201)
async def create(data: UserCreate, db: AsyncSession = Depends(get_db)):
user = User(email=data.email, hashed_password=hash_pw(data.password))
db.add(user)
await db.commit()
return user
@router.get("/{id}", response_model=UserRead)
async def get_(id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, id)
if not user: raise HTTPException(404)
return user
alembic.ini
[alembic]
script_location = migrations
sqlalchemy.url =
transaction_per_migration = false
migrations/env.py
See Alembic Cheatsheet 01
. Key: import models so Base.metadata is populated.
.env
MYAPP_DATABASE_URL=postgresql+asyncpg://app:pass@localhost/app
MYAPP_SECRET_KEY=...
Don’t commit; commit .env.example.
Run
uv sync
alembic upgrade head
uvicorn src.myapp.main:app --reload
Read this next
If you want this exact skeleton as a starter repo, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .