plugins/development/skills/fastapi-workflow/SKILL.md
Docs-first development workflow for Python + FastAPI + Pydantic v2 projects with async APIs, dependency injection, and SQLAlchemy. Fetches current documentation via MCP before any implementation. Use when building or modifying FastAPI backends, API endpoints, Pydantic models, or database operations. Trigger phrases - "fastapi", "python api", "backend api", "pydantic", "sqlalchemy", "async api", "dependency injection". NOT for frontend work (use frontend-app/frontend-lp) or non-Python backends.
npx skillsauth add petrogurcak/skills fastapi-workflowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
DOCS FIRST, CODE SECOND
You MUST NEVER generate FastAPI code without first fetching relevant documentation via MCP tools.
This is NON-NEGOTIABLE. Every workflow below has MANDATORY MCP fetch steps that MUST be completed before implementation.
Use this skill for ALL FastAPI development tasks:
Required Versions:
DEPRECATED:
Key Pydantic v2 Changes:
model_validator instead of validatorfield_validator instead of validatorConfigDict instead of class ConfigALWAYS use Scalar instead of Swagger UI / ReDoc. Scalar provides modern, interactive API docs with dark mode, search, and code snippets.
Setup pattern:
# requirements.txt
scalar-fastapi>=1.6.0
# main.py
from scalar_fastapi import get_scalar_api_reference
app = FastAPI(
title="My API",
docs_url=None, # Disable Swagger UI
redoc_url=None, # Disable ReDoc
)
@app.get("/docs", include_in_schema=False)
async def scalar_docs():
return get_scalar_api_reference(
openapi_url=app.openapi_url,
title="My API",
dark_mode=True,
)
Key points:
docs_url and redoc_url in FastAPI constructor/docs as a custom routeinclude_in_schema=False hides the docs endpoint from the API schema/openapi.json (FastAPI default)Use for: Creating new API routes/endpoints
MANDATORY CHECKLIST:
☐ 1. Determine endpoint type
- GET (retrieve data)
- POST (create data)
- PUT/PATCH (update data)
- DELETE (remove data)
☐ 2. ⚠️ MANDATORY: Fetch FastAPI routing docs
Call MCP tool: fetch_fastapi_docs(topic: "routing")
Wait for response before continuing
☐ 3. ⚠️ MANDATORY: Fetch Pydantic model docs
Call MCP tool: fetch_pydantic_docs(topic: "models")
Wait for response before continuing
☐ 4. Define Pydantic models:
- Request body model (for POST/PUT)
- Response model
- Use Pydantic v2 syntax
☐ 5. Implement endpoint:
- Use APIRouter for organization
- Proper HTTP status codes
- Response model in decorator
- Async def for async operations
☐ 6. Add dependencies:
- Database session
- Authentication
- Rate limiting (if needed)
☐ 7. VERIFY against quality checklist:
✓ Uses FastAPI 0.115+ patterns
✓ Pydantic v2 models
✓ Proper error handling
✓ OpenAPI docs generated
✓ Type hints everywhere
Example Execution:
# WRONG: Generating code without fetching docs
@app.get("/products")
def get_products():
return products
# CORRECT: Workflow followed
# 1. Determined: GET endpoint for products
# 2. Called: fetch_fastapi_docs(topic: "routing")
# 3. Called: fetch_pydantic_docs(topic: "models")
# 4. Reviewed current patterns from docs
# 5. Now implementing with current API:
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict
from typing import Annotated
router = APIRouter(prefix="/products", tags=["products"])
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
price: float
description: str | None = None
@router.get(
"/",
response_model=list[ProductResponse],
summary="Get all products",
)
async def get_products(
db: Annotated[AsyncSession, Depends(get_db)],
skip: int = 0,
limit: int = 100,
) -> list[ProductResponse]:
"""Retrieve all products with pagination."""
products = await db.execute(
select(Product).offset(skip).limit(limit)
)
return products.scalars().all()
Use for: Defining request/response schemas
MANDATORY CHECKLIST:
☐ 1. Identify model purpose:
- Request body (input validation)
- Response model (output serialization)
- Database model (ORM mapping)
- Internal model (business logic)
☐ 2. ⚠️ MANDATORY: Fetch Pydantic v2 documentation
Call MCP tool: fetch_pydantic_docs(topic: "models")
For validators: fetch_pydantic_docs(topic: "validators")
Wait for response before continuing
☐ 3. Define model with Pydantic v2 syntax:
- Use ConfigDict instead of class Config
- Use field_validator instead of validator
- Use model_validator for cross-field validation
- Proper type hints (str | None, not Optional[str])
☐ 4. Add validation:
- Field constraints (min_length, ge, le)
- Custom validators
- Computed fields if needed
☐ 5. Handle serialization:
- from_attributes=True for ORM
- alias for field name mapping
- exclude for sensitive fields
☐ 6. VERIFY model:
✓ Pydantic v2 syntax (not v1)
✓ All fields typed
✓ Validation covers edge cases
✓ Serialization correct
Example - Pydantic v2 Models:
# CORRECT: Pydantic v2 patterns
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from datetime import datetime
class ProductCreate(BaseModel):
"""Request model for creating a product."""
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="Price must be positive")
description: str | None = None
category_id: int
@field_validator('name')
@classmethod
def name_must_be_title_case(cls, v: str) -> str:
return v.strip().title()
class ProductResponse(BaseModel):
"""Response model for product data."""
model_config = ConfigDict(
from_attributes=True, # Enable ORM mode
json_schema_extra={
"example": {
"id": 1,
"name": "Example Product",
"price": 29.99,
}
}
)
id: int
name: str
price: float
description: str | None = None
created_at: datetime
class ProductUpdate(BaseModel):
"""Request model for updating a product."""
name: str | None = None
price: float | None = Field(default=None, gt=0)
description: str | None = None
@model_validator(mode='after')
def check_at_least_one_field(self) -> 'ProductUpdate':
if all(v is None for v in [self.name, self.price, self.description]):
raise ValueError('At least one field must be provided')
return self
Use for: Reusable dependencies (database, auth, etc.)
MANDATORY CHECKLIST:
☐ 1. Identify dependency type:
- Database session
- Current user (authentication)
- Configuration settings
- External service client
☐ 2. ⚠️ MANDATORY: Fetch FastAPI dependency docs
Call MCP tool: fetch_fastapi_docs(topic: "dependencies")
Wait for response before continuing
☐ 3. Implement dependency function:
- Use Annotated for type hints
- Yield for cleanup (database sessions)
- Async for async operations
☐ 4. Use in endpoints:
- Depends() for injection
- Annotated[Type, Depends(dep)] pattern
☐ 5. VERIFY dependency:
✓ Proper cleanup (yield)
✓ Error handling
✓ Reusable across endpoints
Example - Dependencies:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
# Database dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# Type alias for reuse
DbSession = Annotated[AsyncSession, Depends(get_db)]
# Auth dependency
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: DbSession,
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await db.get(User, user_id)
if user is None:
raise credentials_exception
return user
# Type alias for authenticated endpoints
CurrentUser = Annotated[User, Depends(get_current_user)]
# Usage in endpoint
@router.get("/me")
async def get_me(current_user: CurrentUser) -> UserResponse:
return current_user
Use for: Database models and queries
MANDATORY CHECKLIST:
☐ 1. Determine database operation:
- Model definition
- CRUD operations
- Complex queries
- Migrations
☐ 2. ⚠️ MANDATORY: Fetch SQLAlchemy 2.0 docs
Call MCP tool: fetch_sqlalchemy_docs(topic: "async")
Wait for response before continuing
☐ 3. Define SQLAlchemy model:
- Use Mapped and mapped_column (SQLAlchemy 2.0)
- Relationships with proper loading
- Indexes for performance
☐ 4. Implement repository/service:
- Async operations
- Proper transaction handling
- Error handling
☐ 5. VERIFY database code:
✓ SQLAlchemy 2.0 syntax
✓ Async operations
✓ N+1 queries avoided
✓ Transactions handled
Example - SQLAlchemy 2.0:
from sqlalchemy import ForeignKey, String, select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
price: Mapped[float]
description: Mapped[str | None]
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
category: Mapped["Category"] = relationship(back_populates="products")
class Category(Base):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True)
products: Mapped[list["Product"]] = relationship(back_populates="category")
# Repository pattern
class ProductRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get_all(self, skip: int = 0, limit: int = 100) -> list[Product]:
result = await self.session.execute(
select(Product)
.options(selectinload(Product.category))
.offset(skip)
.limit(limit)
)
return list(result.scalars().all())
async def get_by_id(self, product_id: int) -> Product | None:
return await self.session.get(Product, product_id)
async def create(self, product_data: ProductCreate) -> Product:
product = Product(**product_data.model_dump())
self.session.add(product)
await self.session.flush()
return product
Use for: API error responses
MANDATORY CHECKLIST:
☐ 1. ⚠️ MANDATORY: Fetch FastAPI error handling docs
Call MCP tool: fetch_fastapi_docs(topic: "error-handling")
Wait for response before continuing
☐ 2. Define custom exceptions:
- Inherit from Exception
- Include error details
☐ 3. Create exception handlers:
- Register with app
- Return proper HTTP responses
☐ 4. Use HTTPException for simple cases:
- Proper status codes
- Clear error messages
☐ 5. VERIFY error handling:
✓ All errors return JSON
✓ Proper status codes
✓ No sensitive info leaked
Example - Error Handling:
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse
# Custom exception
class ProductNotFoundError(Exception):
def __init__(self, product_id: int):
self.product_id = product_id
# Exception handler
@app.exception_handler(ProductNotFoundError)
async def product_not_found_handler(
request: Request,
exc: ProductNotFoundError,
) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"detail": f"Product with id {exc.product_id} not found",
"error_code": "PRODUCT_NOT_FOUND",
},
)
# Usage in endpoint
@router.get("/{product_id}")
async def get_product(product_id: int, db: DbSession) -> ProductResponse:
product = await db.get(Product, product_id)
if product is None:
raise ProductNotFoundError(product_id)
return product
Use for: API tests with pytest
MANDATORY CHECKLIST:
☐ 1. ⚠️ MANDATORY: Fetch FastAPI testing docs
Call MCP tool: fetch_fastapi_docs(topic: "testing")
Wait for response before continuing
☐ 2. Set up test client:
- Use TestClient or httpx.AsyncClient
- Override dependencies for testing
☐ 3. Write tests:
- Test happy path
- Test error cases
- Test edge cases
☐ 4. Use fixtures:
- Database fixtures
- Auth fixtures
- Factory patterns
☐ 5. VERIFY tests:
✓ All endpoints tested
✓ Error cases covered
✓ No real DB in unit tests
Example - Testing:
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
@pytest.fixture
async def client(db_session: AsyncSession):
"""Test client with overridden dependencies."""
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 client:
yield client
app.dependency_overrides.clear()
@pytest.mark.asyncio
async def test_get_products(client: AsyncClient, sample_products):
response = await client.get("/products/")
assert response.status_code == 200
data = response.json()
assert len(data) == len(sample_products)
@pytest.mark.asyncio
async def test_get_product_not_found(client: AsyncClient):
response = await client.get("/products/99999")
assert response.status_code == 404
assert response.json()["error_code"] == "PRODUCT_NOT_FOUND"
@pytest.mark.asyncio
async def test_create_product(client: AsyncClient, auth_headers):
payload = {"name": "New Product", "price": 29.99, "category_id": 1}
response = await client.post(
"/products/",
json=payload,
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New Product"
# ✓ CORRECT: Modern Python
# Union types with |
name: str | None = None
# Match statements
match status:
case "active":
return "Active"
case "pending":
return "Pending"
case _:
return "Unknown"
# Exception groups (if needed)
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(task1())
tg.create_task(task2())
except* ValueError as eg:
handle_errors(eg.exceptions)
# ✓ CORRECT: Annotated dependencies
from typing import Annotated
DbSession = Annotated[AsyncSession, Depends(get_db)]
@router.get("/items")
async def get_items(db: DbSession) -> list[ItemResponse]:
...
# ✓ CORRECT: Response model in decorator
@router.post("/items", response_model=ItemResponse, status_code=201)
async def create_item(...):
...
# ✓ CORRECT: Tags and summary for OpenAPI
@router.get("/items", tags=["items"], summary="List all items")
async def list_items(...):
"""
Retrieve all items.
- **skip**: Number of items to skip
- **limit**: Maximum number of items to return
"""
...
# ✓ CORRECT: Pydantic v2
from pydantic import BaseModel, ConfigDict, Field, field_validator
class ItemModel(BaseModel):
model_config = ConfigDict(
from_attributes=True,
str_strip_whitespace=True,
)
name: str = Field(min_length=1)
price: float = Field(gt=0)
@field_validator('name')
@classmethod
def validate_name(cls, v: str) -> str:
return v.title()
# ✗ WRONG: Pydantic v1 (deprecated)
class ItemModel(BaseModel):
class Config: # Old style
orm_mode = True
@validator('name') # Old decorator
def validate_name(cls, v):
return v.title()
What operation?
├─ Retrieve data → GET
├─ Create new resource → POST
├─ Full update → PUT
├─ Partial update → PATCH
└─ Remove resource → DELETE
What are you returning?
├─ Single item → ItemResponse
├─ List of items → list[ItemResponse]
├─ Paginated → PaginatedResponse[ItemResponse]
├─ Nothing → None (with 204)
└─ Created item → ItemResponse (with 201)
Query complexity?
├─ Simple CRUD → Direct SQLAlchemy in endpoint
├─ Complex queries → Repository pattern
├─ Business logic → Service layer + Repository
└─ Multiple models → Unit of Work pattern
DON'T:
DO:
This skill works with the FastAPI MCP server. MCP provides current docs, this skill enforces the workflow.
Available MCP Tools:
fetch_fastapi_docs(topic) - FastAPI patterns and featuresfetch_pydantic_docs(topic) - Pydantic v2 models and validationfetch_sqlalchemy_docs(topic) - SQLAlchemy 2.0 async operationsfetch_python_docs(topic) - Python stdlib featuressearch_pypi(query) - Search Python packagesYou MUST call these tools at the workflow steps marked with ⚠️ MANDATORY
Before considering any FastAPI implementation complete:
FINAL VERIFICATION CHECKLIST:
☐ All MANDATORY MCP tool calls were made
☐ Implementation follows fetched documentation
☐ Uses FastAPI 0.115+ patterns
☐ Uses Pydantic v2 syntax
☐ Uses SQLAlchemy 2.0 (if database)
☐ Python 3.11+ features used
☐ All endpoints have response models
☐ Error handling implemented
☐ Type hints everywhere
☐ Tests written and passing
☐ No secrets in code
This skill enforces a DOCS-FIRST development process for FastAPI:
NEVER skip step 2. The MCP server provides current, accurate documentation. Use it.
Your workflows are your process. Follow them rigorously for consistent, high-quality Python backend code that uses current FastAPI, Pydantic v2, and SQLAlchemy 2.0 patterns.
development
Builds a pre-launch social proof strategy through structured beta programs using D'Souza Brain Audit interviews. Use when launching new products/services and need compelling testimonials, planning a beta cohort, designing interview questions to harvest objection-busting social proof, improving video testimonials for landing pages, or designing case studies with metrics. Trigger phrases include "beta tester program for testimonials", "pre-launch social proof", "Brain Audit testimonial framework", "case study harvest", "reverse testimonial", "video testimonial mechanics", "social proof landing page", "sběr referencí", "beta tester program", "testimonial pro landing page", "social proof před launchem", "rozhovor s klientem", "case study sběr", "reference před spuštěním". NOT for ongoing case study production (use growth-hacking case-study approach), offer design (use offer-creation), or conversion optimization (use ux-optimization).
development
Use when planning a product launch and the product type is unclear or could be either generic (SaaS/app/physical) or info-product. Routes between marketing:launch-strategy (generic launches) and marketing:info-product-launch (courses, memberships, ebooks, cohorts, communities). Trigger phrases - "launch", "spuštění", "go-to-market", "product launch", "release strategy", "uvedení na trh", "launch plan", "spuštění produktu", "launch sequence", "launch strategy". Do NOT trigger when product type is already clear (use specific skill directly).
testing
Specialized 8-week launch cadence for info-products — online courses, cohort programs, memberships, communities, ebooks, masterminds. Combines Jeff Walker's Product Launch Formula (Seed/Internal/JV variants, PLC sequence, open-cart day-by-day) with Stu McLaren's membership mechanics (closed cart, Success Path) and Hormozi Grand Slam Offer stacking. Use when planning "launch online kurzu", "info-product launch", "PLF launch", "course launch", "membership launch", "cohort launch", "ebook launch", "open cart close cart", "8-week launch of online course", "beta cohort to launch sequence", "spuštění kurzu", "launch členské sekce", "open cart strategie". Differentiates from marketing:launch-strategy (generic SaaS/app launches) — info-product-specific. NOT for SaaS launches, physical products, or services.
development
Use when releasing an Expo/React Native mobile app to App Store and Google Play - covers eas submit, ASC "Submit for Review", Play promote Internal→Production, OTA update, and decoding common silent failures (Apple agreement expiry, missing English locale, Background Location declaration, web bundle failure on react-native-maps).