python-fastapi

App factory, lifespan

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "python-fastapi" with this command: npx skills add amrahman90/python-expert-agent/amrahman90-python-expert-agent-python-fastapi

Quick Reference

Layer File Purpose

Entrypoint main.py

App factory, lifespan

Config config.py

pydantic-settings

Routes api/v1/endpoints/

Endpoint handlers

Schemas schemas/

Pydantic models

Models models/

SQLAlchemy models

Services services/

Business logic

Deps api/deps.py

Dependency injection

Pydantic Pattern Use Case

UserCreate

Input for creation

UserUpdate

Input for updates

UserResponse

API output

UserInDB

Internal with hash

Dependency Code

DB session db: Annotated[AsyncSession, Depends(get_db)]

Current user user: Annotated[User, Depends(get_current_user)]

Type alias CurrentUser = Annotated[User, Depends(...)]

Production Config

Server gunicorn -w 4 -k uvicorn.workers.UvicornWorker

Workers CPU cores for async

When to Use This Skill

Use for FastAPI development:

  • Setting up FastAPI project structure

  • Creating Pydantic schemas with validation

  • Implementing dependency injection

  • JWT authentication setup

  • Docker deployment configuration

Related skills:

  • For async patterns: see python-asyncio

  • For testing: see python-testing

  • For type hints: see python-type-hints

FastAPI Production Best Practices (2025)

Overview

FastAPI is a modern, high-performance web framework for building APIs. Built on Starlette and Pydantic, it provides automatic validation, serialization, and OpenAPI documentation.

Project Structure

Scalable Structure (Recommended)

src/ ├── app/ │ ├── init.py │ ├── main.py # Application factory │ ├── config.py # Settings management │ ├── database.py # Database setup │ ├── dependencies.py # Shared dependencies │ ├── api/ │ │ ├── init.py │ │ ├── v1/ │ │ │ ├── init.py │ │ │ ├── router.py # API router │ │ │ └── endpoints/ │ │ │ ├── users.py │ │ │ ├── items.py │ │ │ └── auth.py │ │ └── deps.py # API dependencies │ ├── core/ │ │ ├── init.py │ │ ├── security.py # Auth/JWT │ │ └── exceptions.py # Custom exceptions │ ├── models/ # SQLAlchemy models │ │ ├── init.py │ │ ├── user.py │ │ └── item.py │ ├── schemas/ # Pydantic schemas │ │ ├── init.py │ │ ├── user.py │ │ └── item.py │ └── services/ # Business logic │ ├── init.py │ ├── user.py │ └── item.py ├── tests/ ├── pyproject.toml └── Dockerfile

Configuration

Settings with pydantic-settings

app/config.py

from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict

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

# Application
app_name: str = "My API"
debug: bool = False
environment: str = "production"

# Database
database_url: str
database_pool_size: int = 5
database_max_overflow: int = 10

# Security
secret_key: str
access_token_expire_minutes: int = 30
algorithm: str = "HS256"

# External services
redis_url: str | None = None

@lru_cache def get_settings() -> Settings: return Settings()

settings = get_settings()

Application Factory

app/main.py

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

from app.config import settings from app.api.v1.router import api_router from app.database 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()

def create_app() -> FastAPI: app = FastAPI( title=settings.app_name, openapi_url="/api/v1/openapi.json" if settings.debug else None, docs_url="/api/docs" if settings.debug else None, redoc_url="/api/redoc" if settings.debug else None, lifespan=lifespan, )

# Middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"] if settings.debug else ["https://myapp.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Routes
app.include_router(api_router, prefix="/api/v1")

return app

app = create_app()

Pydantic Schemas

Input/Output Separation

app/schemas/user.py

from datetime import datetime from pydantic import BaseModel, EmailStr, Field

Base schema with shared fields

class UserBase(BaseModel): email: EmailStr full_name: str | None = None

Input schema for creation

class UserCreate(UserBase): password: str = Field(min_length=8)

Input schema for updates

class UserUpdate(BaseModel): email: EmailStr | None = None full_name: str | None = None password: str | None = Field(None, min_length=8)

Output schema (what API returns)

class UserResponse(UserBase): id: int is_active: bool created_at: datetime

model_config = {"from_attributes": True}

Internal schema with password hash

class UserInDB(UserBase): id: int hashed_password: str is_active: bool created_at: datetime

model_config = {"from_attributes": True}

Validation Examples

from typing import Annotated from pydantic import BaseModel, Field, field_validator, model_validator

class ItemCreate(BaseModel): name: Annotated[str, Field(min_length=1, max_length=100)] price: Annotated[float, Field(gt=0, description="Price must be positive")] quantity: Annotated[int, Field(ge=0, le=10000)] tags: list[str] = Field(default_factory=list, max_length=10)

@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
    if not v.strip():
        raise ValueError("Name cannot be empty or whitespace")
    return v.strip()

@field_validator("tags")
@classmethod
def tags_must_be_unique(cls, v: list[str]) -> list[str]:
    if len(v) != len(set(v)):
        raise ValueError("Tags must be unique")
    return v

@model_validator(mode="after")
def validate_price_quantity(self) -> "ItemCreate":
    if self.quantity > 0 and self.price < 0.01:
        raise ValueError("Price too low for available items")
    return self

Async Best Practices

Correct Async Usage

from fastapi import FastAPI, Depends from sqlalchemy.ext.asyncio import AsyncSession import httpx

app = FastAPI()

GOOD: Async database operations

async def get_user(db: AsyncSession, user_id: int): result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none()

GOOD: Async HTTP calls

async def fetch_external_data(url: str): async with httpx.AsyncClient() as client: response = await client.get(url) return response.json()

BAD: Blocking call in async route

@app.get("/bad") async def bad_endpoint(): import time time.sleep(5) # Blocks entire event loop! return {"status": "done"}

GOOD: Use asyncio.sleep or run in executor

@app.get("/good") async def good_endpoint(): import asyncio await asyncio.sleep(5) # Non-blocking return {"status": "done"}

GOOD: Run blocking code in executor

@app.get("/cpu-bound") async def cpu_bound(): import asyncio loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, heavy_computation) return {"result": result}

Sync vs Async Routes

Async route - for I/O-bound operations

@app.get("/async-users") async def get_users(db: AsyncSession = Depends(get_db)): # Uses async database session users = await db.execute(select(User)) return users.scalars().all()

Sync route - FastAPI runs in thread pool

@app.get("/sync-compute") def compute_heavy(): # OK for CPU-bound operations # FastAPI automatically runs this in thread pool return {"result": heavy_cpu_computation()}

Dependencies

Dependency Injection

app/api/deps.py

from typing import Annotated from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.ext.asyncio import AsyncSession from jose import JWTError, jwt

from app.database import get_db from app.config import settings from app.models import User

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

async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[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.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 db.get(User, user_id)
if user is None:
    raise credentials_exception
return user

async def get_current_active_user( current_user: Annotated[User, Depends(get_current_user)], ) -> User: if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user

Type aliases for cleaner signatures

CurrentUser = Annotated[User, Depends(get_current_active_user)] DbSession = Annotated[AsyncSession, Depends(get_db)]

Using Dependencies

app/api/v1/endpoints/users.py

from fastapi import APIRouter, HTTPException from app.api.deps import CurrentUser, DbSession from app.schemas.user import UserResponse, UserUpdate

router = APIRouter()

@router.get("/me", response_model=UserResponse) async def read_current_user(current_user: CurrentUser): return current_user

@router.patch("/me", response_model=UserResponse) async def update_current_user( update_data: UserUpdate, current_user: CurrentUser, db: DbSession, ): for field, value in update_data.model_dump(exclude_unset=True).items(): setattr(current_user, field, value) db.add(current_user) await db.commit() await db.refresh(current_user) return current_user

Error Handling

Custom Exception Handlers

app/core/exceptions.py

from fastapi import HTTPException, Request from fastapi.responses import JSONResponse from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR

class AppException(Exception): def init(self, status_code: int, detail: str, error_code: str = None): self.status_code = status_code self.detail = detail self.error_code = error_code

class NotFoundError(AppException): def init(self, resource: str, id: int): super().init( status_code=404, detail=f"{resource} with id {id} not found", error_code="NOT_FOUND", )

class ValidationError(AppException): def init(self, detail: str): super().init( status_code=422, detail=detail, error_code="VALIDATION_ERROR", )

app/main.py

async def app_exception_handler(request: Request, exc: AppException): return JSONResponse( status_code=exc.status_code, content={ "detail": exc.detail, "error_code": exc.error_code, }, )

async def unhandled_exception_handler(request: Request, exc: Exception): # Log the error logger.exception(f"Unhandled error: {exc}") return JSONResponse( status_code=HTTP_500_INTERNAL_SERVER_ERROR, content={"detail": "Internal server error"}, )

app.add_exception_handler(AppException, app_exception_handler) app.add_exception_handler(Exception, unhandled_exception_handler)

Production Deployment

Docker

Dockerfile

FROM python:3.12-slim

WORKDIR /app

Install uv

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

Copy dependency files

COPY pyproject.toml uv.lock ./

Install dependencies

RUN uv sync --frozen --no-dev

Copy application

COPY src/ ./src/

Run with uvicorn

CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Gunicorn with Uvicorn Workers

gunicorn.conf.py

import multiprocessing

Workers = CPU cores (for async)

workers = multiprocessing.cpu_count() worker_class = "uvicorn.workers.UvicornWorker"

Timeouts

timeout = 120 keepalive = 5

Logging

accesslog = "-" errorlog = "-" loglevel = "info"

Binding

bind = "0.0.0.0:8000"

Run in production

gunicorn app.main:app -c gunicorn.conf.py

Health Checks

from fastapi import APIRouter

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

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

@router.get("/ready") async def readiness_check(db: DbSession): try: await db.execute(text("SELECT 1")) return {"status": "ready", "database": "connected"} except Exception: raise HTTPException(status_code=503, detail="Database not ready")

Security

JWT Authentication

app/core/security.py

from datetime import datetime, timedelta from jose import jwt from passlib.context import CryptContext from app.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(subject: int | str, expires_delta: timedelta = None) -> str: if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)

to_encode = {"exp": expire, "sub": str(subject)}
return jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)

Rate Limiting

from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/limited") @limiter.limit("10/minute") async def limited_endpoint(request: Request): return {"message": "This endpoint is rate limited"}

Testing

tests/conftest.py

import pytest from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from app.main import app from app.database import get_db

@pytest.fixture async def client(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: yield client

@pytest.fixture async def authenticated_client(client: AsyncClient, test_user_token: str): client.headers["Authorization"] = f"Bearer {test_user_token}" yield client

tests/test_users.py

import pytest

@pytest.mark.asyncio async def test_create_user(client: AsyncClient): response = await client.post("/api/v1/users", json={ "email": "test@example.com", "password": "testpassword123", }) assert response.status_code == 201 assert response.json()["email"] == "test@example.com"

Additional References

For advanced patterns beyond this guide, see:

  • Dependency Injection Patterns - Type aliases (DbSession, CurrentUser, AdminUser), pagination dependencies, sorting/filtering, service layer patterns, request context, feature flags, rate limiting, caching dependencies

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-asyncio

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-backend

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-package-management

No summary provided by upstream source.

Repository SourceNeeds Review