Scaffold a production-ready async FastAPI application with SQLAlchemy 2.0, Alembic migrations, Pydantic v2, and structured logging.
90
87%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Scaffold a production-ready async FastAPI application with SQLAlchemy 2.0, Alembic migrations, Pydantic v2, and structured logging.
uv or pip for dependency managementmkdir -p src/app/{api/routes,core,db,models,schemas,services} tests
touch src/app/__init__.py src/app/api/__init__.py src/app/api/routes/__init__.py \
src/app/core/__init__.py src/app/db/__init__.py src/app/models/__init__.py \
src/app/schemas/__init__.py src/app/services/__init__.py tests/__init__.py
cat > pyproject.toml << 'PYPROJECT'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"sqlalchemy[asyncio]>=2.0.36",
"asyncpg>=0.30.0",
"alembic>=1.14",
"structlog>=24.4",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.24",
"httpx>=0.27",
"ruff>=0.8",
]
[tool.ruff]
target-version = "py312"
line-length = 99
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM", "ASYNC"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
PYPROJECT
alembic init -t async src/app/db/migrations
# Initialize database
alembic upgrade headproject-root/
├── pyproject.toml
├── .env.example # Required env vars template
├── src/
│ └── app/
│ ├── __init__.py
│ ├── main.py # Application entrypoint + lifespan
│ ├── api/
│ │ ├── __init__.py
│ │ ├── deps.py # Shared dependencies (get_db, get_current_user)
│ │ └── routes/
│ │ ├── __init__.py
│ │ ├── health.py
│ │ └── users.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py # Pydantic Settings
│ │ └── logging.py # structlog configuration
│ ├── db/
│ │ ├── __init__.py
│ │ ├── engine.py # async engine + sessionmaker
│ │ ├── base.py # DeclarativeBase
│ │ └── migrations/ # Alembic
│ │ ├── env.py
│ │ └── versions/
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py # SQLAlchemy ORM models
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── user.py # Pydantic v2 schemas
│ └── services/
│ ├── __init__.py
│ └── user.py # Business logic
└── tests/
├── __init__.py
├── conftest.py
└── test_users.pyasync def for route handlers, services, and DB access.api/routes/. Mount all routers via a parent APIRouter in api/routes/__init__.py.schemas/. ORM models live in models/. Never mix them.pydantic-settings and injected with Depends.env.py imports all models from models/__init__.py to detect changes.structlog for all application logging. No print() statements.from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
app_name: str = "app"
debug: bool = False
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/app"
cors_origins: list[str] = ["http://localhost:3000"]
settings = Settings()from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.core.config import settings
engine = create_async_engine(settings.database_url, echo=settings.debug)
async_session = async_sessionmaker(engine, expire_on_commit=False)from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
class Base(DeclarativeBase, MappedAsDataclass):
"""Base class for all ORM models. Uses mapped_column dataclass style."""
passfrom datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy import DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[UUID] = mapped_column(primary_key=True, default_factory=uuid4, init=False)
email: Mapped[str] = mapped_column(unique=True, index=True)
name: Mapped[str]
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), init=False
)from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, EmailStr, ConfigDict
class UserCreate(BaseModel):
email: EmailStr
name: str
class UserRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
email: str
name: str
is_active: bool
created_at: datetimefrom collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.engine import async_session
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield sessionfrom uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserRead
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(body: UserCreate, db: AsyncSession = Depends(get_db)) -> User:
user = User(email=body.email, name=body.name)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.get("/{user_id}", response_model=UserRead)
async def get_user(user_id: UUID, db: AsyncSession = Depends(get_db)) -> User:
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return userfrom fastapi import APIRouter
from app.api.routes.health import router as health_router
from app.api.routes.users import router as users_router
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(health_router)
api_router.include_router(users_router)from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import api_router
from app.core.config import settings
from app.core.logging import setup_logging
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
setup_logging()
logger = structlog.get_logger()
logger.info("application_startup", app_name=settings.app_name)
yield
logger.info("application_shutdown")
def create_app() -> FastAPI:
app = FastAPI(title=settings.app_name, debug=settings.debug, lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router)
return app
app = create_app()import logging
import structlog
def setup_logging(log_level: str = "INFO") -> None:
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logging.basicConfig(format="%(message)s", level=getattr(logging, log_level))from fastapi import BackgroundTasks
@router.post("/users/{user_id}/welcome")
async def send_welcome(user_id: UUID, background_tasks: BackgroundTasks):
background_tasks.add_task(send_welcome_email, user_id)
return {"status": "queued"}import pytest
from httpx import ASGITransport, AsyncClient
from app.main import create_app
@pytest.fixture
async def client():
app = create_app()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac.env.example to .env and fill in valuespython -m venv .venv && source .venv/bin/activatepip install -e ".[dev]" (or uv sync)alembic -c src/app/db/migrations/alembic.ini upgrade headuvicorn app.main:app --reload --host 0.0.0.0 --port 8000 --app-dir srccurl http://localhost:8000/docs# Install dependencies
uv sync # or: pip install -e ".[dev]"
# Run development server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 --app-dir src
# Create migration
alembic -c src/app/db/migrations/alembic.ini revision --autogenerate -m "add users table"
# Apply migrations
alembic -c src/app/db/migrations/alembic.ini upgrade head
# Run tests
pytest
# Lint and format
ruff check src tests
ruff format src tests/docs. Use openapi-typescript-codegen or orval to generate a typed client from the schema.uvicorn command serves as the production entrypoint; pair with gunicorn -k uvicorn.workers.UvicornWorker for multiple workers.ruff check, pytest, and alembic upgrade head (against a test DB) in pipeline.BackgroundTasks, add arq with a shared Redis and define worker tasks in a workers/ package.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.