CtrlK
BlogDocsLog inGet started
Tessl Logo

fastapi-project-starter

Scaffold a production-ready async FastAPI application with SQLAlchemy 2.0, Alembic migrations, Pydantic v2, and structured logging.

90

Quality

87%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

FastAPI Project Starter

Scaffold a production-ready async FastAPI application with SQLAlchemy 2.0, Alembic migrations, Pydantic v2, and structured logging.

Prerequisites

  • Python 3.12+
  • PostgreSQL 15+ (for asyncpg)
  • uv or pip for dependency management

Scaffold Command

mkdir -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 head

Project Structure

project-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.py

Key Conventions

  • All I/O is async. Use async def for route handlers, services, and DB access.
  • One router per resource file in api/routes/. Mount all routers via a parent APIRouter in api/routes/__init__.py.
  • Pydantic v2 schemas live in schemas/. ORM models live in models/. Never mix them.
  • Settings are loaded once via pydantic-settings and injected with Depends.
  • Database sessions are scoped per-request through a dependency.
  • Alembic env.py imports all models from models/__init__.py to detect changes.
  • Use structlog for all application logging. No print() statements.

Essential Patterns

Configuration (core/config.py)

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()

Database Engine (db/engine.py)

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)

Declarative Base (db/base.py)

from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass


class Base(DeclarativeBase, MappedAsDataclass):
    """Base class for all ORM models. Uses mapped_column dataclass style."""
    pass

ORM Model (models/user.py)

from 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
    )

Pydantic Schemas (schemas/user.py)

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: datetime

Dependencies (api/deps.py)

from 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 session

Router (api/routes/users.py)

from 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 user

Router Aggregation (api/routes/init.py)

from 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)

Lifespan + App Factory (main.py)

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()

Structured Logging (core/logging.py)

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))

Background Task Example

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"}

Test (tests/conftest.py)

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

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Create virtual environment: python -m venv .venv && source .venv/bin/activate
  3. Install dependencies: pip install -e ".[dev]" (or uv sync)
  4. Run database migrations: alembic -c src/app/db/migrations/alembic.ini upgrade head
  5. Start dev server: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 --app-dir src
  6. Test the API: curl http://localhost:8000/docs

Common Commands

# 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

Integration Notes

  • Frontend (React/Next.js): FastAPI auto-generates OpenAPI spec at /docs. Use openapi-typescript-codegen or orval to generate a typed client from the schema.
  • Docker: Use a multi-stage Dockerfile. The uvicorn command serves as the production entrypoint; pair with gunicorn -k uvicorn.workers.UvicornWorker for multiple workers.
  • CI: Run ruff check, pytest, and alembic upgrade head (against a test DB) in pipeline.
  • Celery/ARQ: For heavy background work beyond BackgroundTasks, add arq with a shared Redis and define worker tasks in a workers/ package.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.