AgentStack
SKILL verified MIT Self-run

Fastapi Init

skill-robhowley-py-pit-skills-fastapi-init · by robhowley

Scaffold a complete, production-ready FastAPI project from scratch. Use this skill whenever the user wants to create, initialize, start, or bootstrap a FastAPI service, REST API, or Python web service — even if they just say "new service", "new API", or "new microservice". Handles uv setup, standard FastAPI directory layout, uvicorn runner, click CLI entry point, and a full pytest suite with DI o…

No reviews yet
0 installs
11 views
0.0% view→install

Install

$ agentstack add skill-robhowley-py-pit-skills-fastapi-init

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Fastapi Init? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

fastapi-init

Scaffold a new FastAPI project end-to-end. This skill coordinates with others in the plugin — invoke them at the right steps rather than reinventing what they already encode.

Prerequisite skills

  • uv skill: use for all uv add, uv run, and environment commands — never fall back to pip
  • click-cli skill: consult if the user wants an extended CLI beyond the basic server entry point
  • fastapi-errors skill: the authority on the full error architecture — domain subclasses, error codes, auth error patterns, and existing-repo strategy

Step 1 — Confirm the service name

Ask: "What's the name of this service?"

Use a single snake_case variable {pkg_name} for everything — the project directory, the Python package, the pyproject.toml name, and the CLI command (e.g. my_service). The uv run {pkg_name} serve command will use it directly. Derive {PkgName} as the PascalCase form of {pkg_name} (e.g. my_serviceMyService) — used only for the exception class name.


Step 2 — Initialize with uv

uv init {pkg_name} --app --no-workspace
cd {pkg_name}

Remove the stub file uv generates (hello.py), then add dependencies:

uv add fastapi "uvicorn[standard]" "sqlalchemy[asyncio]" aiosqlite "pydantic-settings" click
uv add --dev pytest pytest-asyncio httpx

After uv init, add the hatchling build backend to pyproject.toml so [project.scripts] works:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["{pkg_name}"]

Step 3 — Directory structure

Build this layout under the project root:

{pkg_name}/
├── pyproject.toml
├── uv.lock
├── {pkg_name}/                         ← Python package, same name as project root
│   ├── __init__.py
│   ├── main.py                # FastAPI app + lifespan
│   ├── cli.py                 # click entry point
│   ├── core/
│   │   ├── __init__.py
│   │   ├── config.py          # pydantic-settings Settings
│   │   └── exceptions.py      # {PkgName}Error base + usage note
│   ├── api/
│   │   ├── __init__.py
│   │   ├── deps.py            # shared Annotated Depends providers
│   │   └── v1/
│   │       ├── __init__.py
│   │       └── routes/
│   │           ├── __init__.py
│   │           └── health.py
│   ├── db/
│   │   ├── __init__.py
│   │   ├── base.py            # Base, TimestampMixin, naming convention
│   │   └── session.py         # engine, AsyncSessionLocal, get_db
│   ├── models/
│   │   └── __init__.py        # SQLAlchemy declarative models
│   └── schemas/
│       ├── __init__.py
│       └── base.py            # APIModel + ReadModel base schemas
└── tests/
    ├── __init__.py
    ├── conftest.py
    └── api/
        └── v1/
            ├── __init__.py
            └── test_health.py

Step 4 — File templates

pyproject.toml

After uv init, add these sections (the build-system block from Step 2 plus):

[project.scripts]
{pkg_name} = "{pkg_name}.cli:cli"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

{pkg_name}/core/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="{PKG_NAME}_", env_file=".env", extra="ignore",
    )

    app_name: str = "{pkg_name}"
    debug: bool = False
    database_url: str = "sqlite+aiosqlite:///./app.db"

settings = Settings()

{pkg_name}/db/base.py

Schema infrastructure - Base class, naming convention, and shared mixins. Models import Base from here.

from datetime import datetime, timezone

from sqlalchemy import DateTime, MetaData
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

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

def utcnow() -> datetime:
    return datetime.now(timezone.utc)

class Base(DeclarativeBase):
    metadata = MetaData(naming_convention=convention)

class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=utcnow, onupdate=utcnow,
    )

{pkg_name}/db/session.py

Connection infrastructure. get_db is the single canonical source of truth for database sessions in routes and tests - this is what makes DI overrides in tests work cleanly. Background tasks run outside the FastAPI DI lifecycle and must open sessions via AsyncSessionLocal directly.

from collections.abc import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from {pkg_name}.core.config import settings

engine = create_async_engine(settings.database_url)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        yield session

expire_on_commit=False prevents lazy-load errors when accessing attributes after a commit on async sessions - SQLAlchemy cannot implicitly issue blocking I/O in an async context.

{pkg_name}/api/deps.py

from typing import Annotated

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from {pkg_name}.db.session import get_db

DbSession = Annotated[AsyncSession, Depends(get_db)]

Use DbSession as a type annotation on route parameters — it's self-documenting and avoids repeating Depends(get_db) everywhere.

{pkg_name}/core/exceptions.py

Base class with status_code and detail as class-level defaults, overridable per-instance. Subclasses only need to override the class attributes — no __init__ boilerplate needed.

class {PkgName}Error(Exception):
    status_code: int = 500
    detail: str = "An unexpected error occurred."

    def __init__(self, detail: str | None = None, status_code: int | None = None, **context):
        self.detail = detail if detail is not None else self.__class__.detail
        self.status_code = status_code if status_code is not None else self.__class__.status_code
        self.context = context

Example subclass:

class NotFoundError({PkgName}Error):
    status_code = 404
    detail = "Resource not found."

See the fastapi-errors skill for the full error architecture: domain subclasses, error codes, auth error patterns, and existing-repo strategy.

{pkg_name}/schemas/base.py

from pydantic import BaseModel, ConfigDict

class APIModel(BaseModel):
    model_config = ConfigDict(
        extra="forbid",
        str_strip_whitespace=True,
        validate_assignment=True,
        use_enum_values=True,
        populate_by_name=True,
    )

class ReadModel(APIModel):
    model_config = APIModel.model_config.copy()
    model_config["from_attributes"] = True

class CreateModel(APIModel):
    """Request body for resource creation."""

class UpdateModel(APIModel):
    """Partial update payload. Use model_dump(exclude_unset=True) in the service layer."""

class QueryModel(APIModel):
    """Search, list, and filtering inputs."""
    model_config = APIModel.model_config.copy()
    model_config["extra"] = "ignore"

class CommandModel(APIModel):
    """Action-oriented request body for non-CRUD endpoints."""

{pkg_name}/main.py

Use the lifespan pattern — on_event is deprecated.

from contextlib import asynccontextmanager

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

from {pkg_name}.api.v1.routes import health
from {pkg_name}.core.config import settings
from {pkg_name}.core.exceptions import {PkgName}Error

@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup: run migrations, warm caches, etc.
    yield
    # shutdown: close connections, flush buffers, etc.

app = FastAPI(title=settings.app_name, lifespan=lifespan)
app.include_router(health.router, prefix="/api/v1")

# see fastapi-errors skill for extended patterns (context logging, error codes, auth errors)
@app.exception_handler({PkgName}Error)
async def {pkg_name}_error_handler(request: Request, exc: {PkgName}Error) -> JSONResponse:
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})

@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
    return JSONResponse(status_code=422, content={"detail": "Invalid request data", "details": exc.errors()})

@app.exception_handler(Exception)
async def unexpected_error_handler(request: Request, exc: Exception) -> JSONResponse:
    return JSONResponse(status_code=500, content={"detail": "An unexpected error occurred."})

{pkg_name}/api/v1/routes/health.py

from fastapi import APIRouter

router = APIRouter(tags=["health"])

@router.get("/health")
async def health_check():
    return {"status": "ok"}

{pkg_name}/cli.py

import click
import uvicorn

from {pkg_name}.core.config import settings

@click.group()
def cli():
    """{pkg_name} service CLI."""

@cli.command()
@click.option("--host", default="0.0.0.0", show_default=True, help="Bind host.")
@click.option("--port", default=8000, show_default=True, type=int, help="Bind port.")
@click.option("--reload", is_flag=True, default=settings.debug, help="Enable hot reload.")
def serve(host: str, port: int, reload: bool):
    """Start the uvicorn server."""
    uvicorn.run("{pkg_name}.main:app", host=host, port=port, reload=reload)

Step 5 — Test setup

tests/conftest.py

The test suite is built around three layered async fixtures. The design principle: never hit a real database, never reach outside the process, override DI at the boundary.

import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from {pkg_name}.db.base import Base
from {pkg_name}.db.session import get_db
from {pkg_name}.main import app

TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"

@pytest_asyncio.fixture(scope="session")
async def engine():
    eng = create_async_engine(TEST_DATABASE_URL)
    async with eng.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield eng
    async with eng.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await eng.dispose()

@pytest_asyncio.fixture
async def db_session(engine) -> AsyncSession:
    async with engine.connect() as conn:
        async with conn.begin() as trans:
            session = async_sessionmaker(bind=conn, expire_on_commit=False)()
            yield session
            await session.close()
            await trans.rollback()

@pytest_asyncio.fixture
async def client(db_session: AsyncSession) -> AsyncClient:
    async def override_get_db():
        yield db_session

    app.dependency_overrides[get_db] = override_get_db
    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as c:
        yield c
    app.dependency_overrides.clear()

tests/api/v1/test_health.py

from httpx import AsyncClient

async def test_health_returns_ok(client: AsyncClient):
    response = await client.get("/api/v1/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

Test invariants

Hold these across every test file in the project:

  1. Async tests — all test functions are async def; asyncio_mode = "auto" means no per-test markers needed
  2. No test classes — top-level test_* functions only; pytest_asyncio fixtures handle all setup and teardown
  3. No real databases — all DB-touching tests use the db_session fixture; async SQLite in-memory only
  4. DI overrides, not patches — swap behavior via app.dependency_overrides; don't mock internals
  5. Shared fixtures in conftest.py — test files stay clean; fixtures go in conftest
  6. Transaction-per-test isolation — the rollback in db_session ensures tests never affect each other even if they write data
  7. AsyncClient, not TestClient — use httpx.AsyncClient with ASGITransport for endpoint tests

Step 6 — Verify the scaffold

uv run pytest
uv run {pkg_name} serve --help

Both should succeed with no errors before handing the project to the user.


What's next

The scaffold is ready to extend. Common next steps:

  • Add models - use the sqlalchemy-models skill to define ORM entities under models/
  • Initialize migrations - use the alembic-migrations skill to set up Alembic after adding models
  • Add schemas - use the pydantic-schemas skill for request/response schemas beyond the base classes

Completion checklist

  • [ ] [project.scripts] entry in pyproject.toml points to {pkg_name}.cli:cli
  • [ ] asyncio_mode = "auto" set in [tool.pytest.ini_options]
  • [ ] hatchling build backend present with packages = ["{pkg_name}"]
  • [ ] get_db is the single source of truth for DB sessions — routes and tests both go through it
  • [ ] app.dependency_overrides is cleared after every test fixture that sets it
  • [ ] No test touches a real database, network socket, or Docker container
  • [ ] All test functions are top-level — no test classes
  • [ ] {PkgName}Error base class present in core/exceptions.py and handler registered in main.py
  • [ ] uv run pytest passes from the project root

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.