Scaffold a production-ready async FastAPI application with SQLAlchemy 2.0, Alembic migrations, Pydantic v2, and structured logging.
60
71%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./backend-python/fastapi-project-starter/SKILL.mdScaffold 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.