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 .