| name | FastAPI Expert |
| description | Expert in FastAPI framework for building high-performance async Python APIs. Use when working with FastAPI, async Python, Pydantic, type hints, automatic API documentation, WebSockets, or when the user mentions FastAPI, async APIs, or modern Python web development. |
FastAPI Expert
A specialized skill for building high-performance, production-ready APIs with FastAPI. This skill covers async programming, Pydantic models, dependency injection, authentication, and FastAPI best practices.
Instructions
Core Workflow
Understand requirements
- Identify API type (REST, WebSockets, GraphQL)
- Determine database needs (async SQLAlchemy, Tortoise-ORM, MongoDB)
- Understand authentication requirements (OAuth2, JWT)
- Identify performance requirements
Project structure
- Organize with proper router structure
- Implement dependency injection
- Use Pydantic models for validation
- Configure CORS and middleware
Implement features
- Create path operations with proper type hints
- Implement Pydantic schemas for validation
- Add authentication and authorization
- Use background tasks for async operations
- Implement WebSockets if needed
Testing and documentation
- Write tests using pytest and httpx
- Leverage automatic Swagger/OpenAPI docs
- Configure for production deployment
FastAPI Project Structure
myapp/
├── main.py # Application entry point
├── core/
│ ├── config.py # Settings with Pydantic
│ ├── security.py # Auth utilities
│ └── dependencies.py # Shared dependencies
├── api/
│ ├── v1/
│ │ ├── __init__.py
│ │ ├── api.py # Main API router
│ │ └── endpoints/
│ │ ├── users.py
│ │ ├── auth.py
│ │ └── items.py
├── models/ # SQLAlchemy models
│ ├── __init__.py
│ ├── user.py
│ └── item.py
├── schemas/ # Pydantic schemas
│ ├── __init__.py
│ ├── user.py
│ └── item.py
├── crud/ # Database operations
│ ├── __init__.py
│ ├── user.py
│ └── item.py
├── db/
│ ├── base.py # Database session
│ └── init_db.py # Initial data
└── tests/
├── conftest.py
├── test_users.py
└── test_auth.py
Configuration with Pydantic Settings
# core/config.py
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
# App
PROJECT_NAME: str = "My FastAPI App"
VERSION: str = "1.0.0"
API_V1_STR: str = "/api/v1"
# Security
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Database
DATABASE_URL: str
# CORS
BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000"]
# Redis
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
Pydantic Schemas
# schemas/user.py
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
full_name: Optional[str] = Field(None, max_length=100)
is_active: bool = True
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
@validator('password')
def password_strength(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
return v
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = Field(None, min_length=3, max_length=50)
full_name: Optional[str] = Field(None, max_length=100)
password: Optional[str] = Field(None, min_length=8)
class UserInDBBase(UserBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True # For SQLAlchemy models
class User(UserInDBBase):
"""User response model (excludes password)"""
pass
class UserInDB(UserInDBBase):
"""User model with hashed password"""
hashed_password: str
Database Models (SQLAlchemy)
# models/user.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
full_name = Column(String)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# db/base.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
from core.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base()
# Dependency
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
CRUD Operations
# crud/user.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from models.user import User
from schemas.user import UserCreate, UserUpdate
from core.security import get_password_hash
from fastapi import HTTPException, status
async def get_user(db: AsyncSession, user_id: int) -> User | None:
result = await db.execute(select(User).where(User.id == user_id))
return result.scalar_one_or_none()
async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none()
async def get_users(
db: AsyncSession, skip: int = 0, limit: int = 100
) -> list[User]:
result = await db.execute(select(User).offset(skip).limit(limit))
return result.scalars().all()
async def create_user(db: AsyncSession, user: UserCreate) -> User:
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
username=user.username,
full_name=user.full_name,
hashed_password=hashed_password,
is_active=user.is_active
)
try:
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
except IntegrityError:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this email or username already exists"
)
async def update_user(
db: AsyncSession, user_id: int, user_update: UserUpdate
) -> User:
db_user = await get_user(db, user_id)
if not db_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
update_data = user_update.model_dump(exclude_unset=True)
if "password" in update_data:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
for field, value in update_data.items():
setattr(db_user, field, value)
await db.commit()
await db.refresh(db_user)
return db_user
async def delete_user(db: AsyncSession, user_id: int) -> bool:
db_user = await get_user(db, user_id)
if not db_user:
return False
await db.delete(db_user)
await db.commit()
return True
API Endpoints with Proper Type Hints
# api/v1/endpoints/users.py
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from db.base import get_db
from schemas.user import User, UserCreate, UserUpdate
from crud import user as crud_user
from core.dependencies import get_current_active_user
router = APIRouter()
@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(
*,
db: Annotated[AsyncSession, Depends(get_db)],
user_in: UserCreate,
) -> User:
"""
Create new user.
- **email**: valid email address (unique)
- **username**: 3-50 characters (unique)
- **password**: minimum 8 characters with uppercase, lowercase, and number
- **full_name**: optional full name
"""
# Check if user exists
existing_user = await crud_user.get_user_by_email(db, email=user_in.email)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this email already exists",
)
user = await crud_user.create_user(db=db, user=user_in)
return user
@router.get("/", response_model=list[User])
async def read_users(
db: Annotated[AsyncSession, Depends(get_db)],
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
current_user: Annotated[User, Depends(get_current_active_user)] = None,
) -> list[User]:
"""
Retrieve users with pagination.
- **skip**: number of records to skip (default: 0)
- **limit**: maximum number of records to return (default: 100, max: 100)
"""
users = await crud_user.get_users(db, skip=skip, limit=limit)
return users
@router.get("/{user_id}", response_model=User)
async def read_user(
user_id: int,
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
"""
Get user by ID.
"""
db_user = await crud_user.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return db_user
@router.patch("/{user_id}", response_model=User)
async def update_user(
user_id: int,
user_in: UserUpdate,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)] = None,
) -> User:
"""
Update user.
"""
# Check authorization (users can only update themselves unless admin)
if current_user.id != user_id and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
db_user = await crud_user.update_user(db, user_id=user_id, user_update=user_in)
return db_user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: int,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)] = None,
) -> None:
"""
Delete user.
"""
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
success = await crud_user.delete_user(db, user_id=user_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
Authentication with JWT
# core/security.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
# core/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 core.config import settings
from core.security import verify_password
from db.base import get_db
from crud import user as crud_user
from schemas.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
async def get_current_user(
db: AsyncSession = Depends(get_db),
token: str = Depends(oauth2_scheme)
) -> 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.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await crud_user.get_user(db, user_id=user_id)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user)
) -> User:
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
# api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import timedelta
from db.base import get_db
from core.config import settings
from core.security import create_access_token, verify_password
from crud import user as crud_user
from schemas.user import User
router = APIRouter()
@router.post("/login")
async def login(
db: AsyncSession = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends()
):
"""
OAuth2 compatible token login.
"""
user = await crud_user.get_user_by_email(db, email=form_data.username)
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user.id)}, expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": User.model_validate(user)
}
@router.get("/me", response_model=User)
async def read_users_me(
current_user: User = Depends(get_current_active_user)
):
"""
Get current user.
"""
return current_user
Background Tasks
from fastapi import BackgroundTasks
from typing import Annotated
def send_email(email: str, message: str):
"""Simulated email sending"""
print(f"Sending email to {email}: {message}")
# Actual email sending logic here
@router.post("/send-notification/")
async def send_notification(
email: str,
background_tasks: BackgroundTasks
):
background_tasks.add_task(send_email, email, "Welcome to our service!")
return {"message": "Notification sent in the background"}
WebSocket Support
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@router.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"Client #{client_id} says: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Client #{client_id} left the chat")
Main Application Setup
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from core.config import settings
from api.v1.api import api_router
from db.base import engine, Base
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown
await engine.dispose()
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
lifespan=lifespan
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
Testing
# tests/conftest.py
import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from main import app
from db.base import Base, get_db
from core.config import settings
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest_asyncio.fixture
async def db_session():
engine = create_async_engine(TEST_DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with async_session() as session:
yield session
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest_asyncio.fixture
async def client(db_session):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
# tests/test_users.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
response = await client.post(
"/api/v1/users/",
json={
"email": "test@example.com",
"username": "testuser",
"password": "Test123456",
"full_name": "Test User"
}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert data["username"] == "testuser"
assert "id" in data
assert "hashed_password" not in data
@pytest.mark.asyncio
async def test_read_users(client: AsyncClient):
# Create a user first
await client.post(
"/api/v1/users/",
json={
"email": "test@example.com",
"username": "testuser",
"password": "Test123456"
}
)
# Login to get token
response = await client.post(
"/api/v1/auth/login",
data={"username": "test@example.com", "password": "Test123456"}
)
token = response.json()["access_token"]
# Get users
response = await client.get(
"/api/v1/users/",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
assert len(response.json()) > 0
Critical Rules
Always Do
- Use type hints everywhere (FastAPI requires them)
- Leverage Pydantic for validation and serialization
- Use async/await for all I/O operations
- Implement proper dependency injection
- Use background tasks for long-running operations
- Document all endpoints (FastAPI auto-generates OpenAPI)
- Handle errors with proper HTTP status codes
- Use environment variables for configuration
- Implement proper authentication and authorization
Never Do
- Never use synchronous database operations
- Never skip type hints (FastAPI won't work properly)
- Never store passwords in plain text
- Never expose sensitive data in responses
- Never ignore Pydantic validation errors
- Never mix sync and async code improperly
- Never hardcode configuration values
- Never skip error handling
Knowledge Base
- FastAPI Core: Path operations, dependencies, middleware, background tasks
- Async Python: asyncio, async/await, async database drivers
- Pydantic: Models, validation, serialization, settings management
- SQLAlchemy: Async ORM, relationships, migrations with Alembic
- Authentication: OAuth2, JWT, password hashing
- Testing: pytest, pytest-asyncio, httpx
- Performance: Async operations, caching, connection pooling
- Documentation: Automatic OpenAPI/Swagger generation
Integration with Other Skills
- Works with: Fullstack Guardian, Test Master, DevOps Engineer
- Complements: Django Expert (alternative Python framework), Code Documenter
Best Practices Summary
- Type Hints: Essential for FastAPI functionality
- Async Everything: Use async for I/O operations
- Pydantic Models: Validate all input/output
- Dependency Injection: Use FastAPI's DI system
- Auto Documentation: Leverage automatic OpenAPI docs
- Error Handling: Proper HTTP exceptions
- Security: JWT authentication, password hashing
- Testing: Comprehensive async tests
- Performance: Background tasks, caching, connection pooling
- Configuration: Pydantic Settings for environment management