Skip to content
cccskills
FreePublished on GitHub

fastapi-conventions

FastAPI web framework conventions: APIRouter with prefix/tags/dependencies, Pydantic v2 schemas (BaseModel, model_config, field_validator, model_validator), dependency injection with Depends, async SQLAlchemy session yielding, OAuth2PasswordBearer + JWT auth, lifespan context manager, pydantic-settings configuration, OpenAPI customization, and HTTPException error handling. Activated automatically by fastapi-plugin/stack.md. Use this skill to: - Structure FastAPI apps with per-feature APIRouter modules and a central app factory. - Write Pydantic v2 request and response schemas with proper validators. - Build a reusable Depends-based dependency chain for DB session, auth, and pagination. - Implement JWT-based authentication with get_current_user dependency. - Configure the app from environment variables via pydantic-settings. Do NOT use this skill for: - SQLAlchemy ORM model configuration and Alembic migrations — see fastapi-plugin:sqlalchemy-patterns. - Python language idioms — see python-foundation:python-conventions. - Testing patterns — see python-foundation:pytest-testing.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md12.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

FastAPI Conventions

Detection

Read pyproject.toml before writing any code:

  • Find the FastAPI version under [project.dependencies] or [tool.poetry.dependencies]. FastAPI 0.100+ uses Pydantic v2 by default — this is the assumed baseline.
  • Find the SQLAlchemy version. 2.0+ uses Mapped/mapped_column syntax. If the project uses SQLAlchemy 1.x, note it and use Column()/relationship() style instead.
grep -E "fastapi|sqlalchemy" pyproject.toml

App factory and lifespan

Use a create_app() factory that returns a configured FastAPI instance. Use the @asynccontextmanager lifespan pattern (introduced in FastAPI 0.93+) for startup/shutdown logic.

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.core.config import settings
from app.db.session import engine
from app.users.router import router as users_router
from app.auth.router import router as auth_router


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: initialize DB connection pool, caches, etc.
    async with engine.begin() as conn:
        pass  # pool warm-up; alembic manages schema
    yield
    # Shutdown: close DB pool, flush caches, etc.
    await engine.dispose()


def create_app() -> FastAPI:
    app = FastAPI(
        title=settings.APP_NAME,
        version=settings.APP_VERSION,
        lifespan=lifespan,
    )

    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.CORS_ALLOWED_ORIGINS,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    app.include_router(auth_router)
    app.include_router(users_router)

    return app


app = create_app()

APIRouter — per-feature modules

Create one APIRouter per feature module. Group related endpoints under a shared prefix and tags. Apply shared dependencies (e.g., auth) at the router level to avoid repeating them on every endpoint.

# app/users/router.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.session import get_db
from app.auth.dependencies import get_current_user
from app.users import schemas, service
from app.users.models import User

router = APIRouter(
    prefix="/users",
    tags=["users"],
    dependencies=[Depends(get_current_user)],  # all endpoints require auth
)


@router.get("/me", response_model=schemas.UserRead)
async def get_current_user_profile(
    current_user: User = Depends(get_current_user),
) -> User:
    return current_user


@router.put("/me", response_model=schemas.UserRead)
async def update_profile(
    body: schemas.UserUpdate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> User:
    return await service.update_user(db, current_user.id, body)


@router.get("/{user_id}", response_model=schemas.UserRead)
async def get_user(
    user_id: int,
    db: AsyncSession = Depends(get_db),
) -> User:
    user = await service.get_user_by_id(db, user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    return user

Include routers in the app factory:

app.include_router(users_router)
app.include_router(auth_router, prefix="/auth")

Pydantic v2 schemas

All request/response schemas use Pydantic v2 BaseModel. Use model_config = ConfigDict(from_attributes=True) for ORM-to-schema conversion. Separate Create, Update, and Read schemas when field sets differ.

# app/users/schemas.py
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator


class UserBase(BaseModel):
    email: EmailStr
    display_name: Annotated[str, Field(min_length=2, max_length=100)]


class UserCreate(UserBase):
    password: Annotated[str, Field(min_length=8)]

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if not any(c.isupper() for c in v):
            raise ValueError("Password must contain at least one uppercase letter")
        return v


class UserUpdate(BaseModel):
    display_name: Annotated[str, Field(min_length=2, max_length=100)] | None = None
    email: EmailStr | None = None


class UserRead(UserBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    created_at: datetime
    is_active: bool


class UserWithOrders(UserRead):
    orders: list["OrderRead"] = []

    @model_validator(mode="after")
    def check_active_has_orders(self) -> "UserWithOrders":
        if not self.is_active and self.orders:
            raise ValueError("Inactive users should not have active orders")
        return self

Always set response_model= on each endpoint to control what fields are exposed. Pydantic v2 will strip fields not declared in the response schema.


Dependency injection

Build a reusable dependency chain. Each dependency is a plain callable (function or class) that FastAPI resolves via Depends().

# app/db/session.py
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.core.config import settings

engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
# app/auth/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.db.session import get_db
from app.users.models import User
from app.users import service

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")


async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
        user_id: int | None = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = await service.get_user_by_id(db, int(user_id))
    if user is None:
        raise credentials_exception
    return user


async def require_admin(current_user: User = Depends(get_current_user)) -> User:
    if not current_user.is_admin:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
    return current_user

Override dependencies in tests:

from app.main import app
from app.db.session import get_db

app.dependency_overrides[get_db] = get_test_db

OAuth2 + JWT

Use OAuth2PasswordBearer for token extraction from the Authorization: Bearer header. Issue JWTs with an expiry claim.

# app/auth/service.py
from datetime import datetime, timedelta, timezone
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)


def hash_password(plain: str) -> str:
    return pwd_context.hash(plain)


def create_access_token(subject: str | int, expires_delta: timedelta | None = None) -> str:
    expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
    payload = {"sub": str(subject), "exp": expire}
    return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
# app/auth/router.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession

from app.auth import service as auth_service
from app.auth.schemas import Token
from app.db.session import get_db
from app.users import service as user_service

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


@router.post("/auth/token", response_model=Token)
async def login(
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: AsyncSession = Depends(get_db),
) -> Token:
    user = await user_service.get_user_by_email(db, form_data.username)
    if user is None or not auth_service.verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    token = auth_service.create_access_token(subject=user.id)
    return Token(access_token=token, token_type="bearer")

Never store JWT_SECRET in source code. Read from pydantic-settings Settings(BaseSettings) via environment variable.


HTTPException and error handling

Use HTTPException for expected domain errors. Use custom exception handlers for application-wide error types.

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse


class ItemNotFoundError(Exception):
    def __init__(self, item_id: int) -> None:
        self.item_id = item_id


app = FastAPI()


@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError) -> JSONResponse:
    return JSONResponse(
        status_code=404,
        content={"detail": f"Item {exc.item_id} not found"},
    )


# In a router endpoint:
raise HTTPException(status_code=404, detail="User not found")

# FastAPI returns 422 Unprocessable Entity automatically for Pydantic validation errors.
# The response body looks like:
# {
#   "detail": [
#     {"loc": ["body", "email"], "msg": "value is not a valid email address", "type": "value_error"}
#   ]
# }

pydantic-settings configuration

# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", case_sensitive=False)

    APP_NAME: str = "My FastAPI App"
    APP_VERSION: str = "0.1.0"

    DATABASE_URL: str  # required — must be set in env
    JWT_SECRET: str    # required — must be set in env
    JWT_ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    CORS_ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]


settings = Settings()

Never set DATABASE_URL or JWT_SECRET as literals in config.py. Always require them from the environment.


Checklist before completing development phase

  • Every endpoint has response_model= to filter the output schema
  • Every state-changing endpoint (POST, PUT, PATCH, DELETE) requires Depends(get_current_user) or an explicit BA-approved exemption
  • Settings(BaseSettings) reads all secrets from env — no literals
  • pydantic-settings and python-jose (or authlib) are listed in pyproject.toml dependencies
  • python -c "from app.main import app" passes without import errors
  • No alembic revision or migration files created — that is alembic-specialist's job

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill