$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for FastAPI Projects

FastAPI's type-driven approach is its superpower — AI skips type hints and Pydantic models, generating Flask-style code. Rules for async, dependency injection, and OpenAPI.

8 min read·December 12, 2024

FastAPI's types drive validation, docs, and DI — AI skips all three

Pydantic models, Depends() injection, async patterns, and automatic OpenAPI

Why FastAPI Needs Rules That Enforce Its Type System

FastAPI's design philosophy is 'type hints drive everything' — Pydantic models define request/response schemas, type annotations generate OpenAPI docs, and dependency injection replaces global state. AI assistants ignore all of this, generating Flask-style code: untyped endpoints, manual JSON parsing, global database connections, and no Pydantic models. The code works but misses every FastAPI advantage.

The result: no automatic validation (Pydantic validates for free), no auto-generated API docs (OpenAPI from type hints), no type-safe dependency injection (FastAPI's DI system), and sync endpoints that block the async event loop. It's Flask code running on FastAPI with extra overhead and zero benefit.

These rules target FastAPI 0.100+ with Python 3.12+, Pydantic v2, SQLAlchemy 2.0, and async patterns. Specify your database and async approach.

Rule 1: Pydantic Models for All I/O

The rule: 'Use Pydantic BaseModel for every request body, response body, and query parameter set. Define separate models for input (Create/Update) and output (Response/Detail). Never use dict or raw JSON in endpoint signatures — FastAPI can't validate or document untyped data. Use Pydantic's Field() for validation constraints: Field(min_length=1, max_length=100).'

For model design: 'Create a base model with shared fields, then extend for specific operations: class UserBase(BaseModel): name: str; email: EmailStr. class UserCreate(UserBase): password: str. class UserResponse(UserBase): id: int; created_at: datetime; model_config = ConfigDict(from_attributes=True). Use from_attributes for ORM model conversion.'

For response models: 'Specify response_model on every endpoint: @app.get("/users/{id}", response_model=UserResponse). This filters the response to only include fields in the model — preventing accidental exposure of internal fields (password hash, internal IDs). FastAPI serializes automatically using the model.'

  • Pydantic BaseModel for all request/response bodies — never dict
  • Separate models: UserCreate (input), UserResponse (output), UserUpdate (partial)
  • Field() for validation: min_length, max_length, ge, le, regex
  • response_model on every endpoint — filters output, prevents field leakage
  • from_attributes = True for ORM model → Pydantic model conversion
💡 response_model Filters Output

response_model=UserResponse on an endpoint filters the response to only those fields — preventing accidental exposure of password_hash, internal IDs, or database metadata. One parameter, complete output safety.

Rule 2: Dependency Injection for Everything

The rule: 'Use FastAPI's Depends() for all shared dependencies: database sessions, authentication, configuration, services. Define dependencies as functions or classes: async def get_db(): async with SessionLocal() as session: yield session. Inject in endpoints: def get_user(db: AsyncSession = Depends(get_db)). Never use global variables for database connections, config, or state.'

For authentication: 'Create an auth dependency: async def get_current_user(token: str = Depends(oauth2_scheme)): user = await verify_token(token); if not user: raise HTTPException(401); return user. Inject in protected endpoints: def get_profile(user: User = Depends(get_current_user)). This centralizes auth — no repeated token checking.'

For service layer: 'Create service classes that accept dependencies: class UserService: def __init__(self, db: AsyncSession): self.db = db. Create a dependency: def get_user_service(db: AsyncSession = Depends(get_db)): return UserService(db). Endpoints receive the service, not the raw database session.'

Depends() Replaces Globals

FastAPI's Depends() is dependency injection that's type-safe, testable, and scoped per request. Database sessions, auth, services — all injected, never imported as globals. Makes testing trivial: override dependencies in tests.

Rule 3: Async Endpoints and Database Access

The rule: 'Use async def for all endpoints that perform I/O (database, HTTP, file). Use async database drivers: asyncpg for PostgreSQL, aiosqlite for SQLite. Use SQLAlchemy 2.0 async with AsyncSession. Never call synchronous I/O in async endpoints — it blocks the event loop. If you must call sync code, use run_in_executor or a background task.'

For SQLAlchemy: 'Use SQLAlchemy 2.0 style: async with async_session() as session: result = await session.execute(select(User).where(User.id == id)). Use select() instead of session.query() (2.0 style). Use AsyncSession for all database operations. Define models with DeclarativeBase and Mapped types: class User(Base): id: Mapped[int] = mapped_column(primary_key=True).'

For background tasks: 'Use FastAPI's BackgroundTasks for fire-and-forget operations: def send_email(background_tasks: BackgroundTasks): background_tasks.add_task(send_welcome_email, user.email). For heavy processing, use Celery or ARQ — not background tasks (they share the request worker).'

⚠️ Sync in Async = Blocked

Calling synchronous database drivers (psycopg2, sqlite3) in async endpoints blocks the event loop — all other requests wait. Use asyncpg, aiosqlite, or run_in_executor. AI generates sync calls in async endpoints constantly.

Rule 4: Error Handling and HTTP Exceptions

The rule: 'Use HTTPException for expected errors: raise HTTPException(status_code=404, detail="User not found"). Use custom exception handlers for domain errors: @app.exception_handler(DomainError). Never return error information in a 200 response — use proper HTTP status codes. Use Pydantic's ValidationError handling (automatic 422) for input validation errors.'

For structured errors: 'Define error response models: class ErrorResponse(BaseModel): detail: str; code: str. Use responses parameter on endpoints to document error responses: @app.get("/users/{id}", responses={404: {"model": ErrorResponse}}). This adds error schemas to OpenAPI docs — clients know exactly what errors to expect.'

For validation errors: 'FastAPI automatically returns 422 with Pydantic validation details. Customize with an exception handler if you need a different format: @app.exception_handler(RequestValidationError). The default format includes the field, location (body/query/path), and error message — usually sufficient.'

Rule 5: OpenAPI Documentation

The rule: 'FastAPI auto-generates OpenAPI docs from your type hints and Pydantic models. Enhance with: tags for grouping endpoints, summary for short description, description for detailed explanation, response_description for response docs. Use docstrings for endpoint descriptions: """Get a user by ID. Returns 404 if the user doesn't exist.""".'

For organization: 'Group endpoints with tags: @app.get("/users", tags=["users"]). Define tag metadata in the app: app = FastAPI(openapi_tags=[{"name": "users", "description": "User operations"}]). Tags group endpoints in Swagger UI and ReDoc — making the API navigable.'

For deployment: 'Swagger UI is at /docs, ReDoc at /redoc. In production, disable docs if the API is internal: app = FastAPI(docs_url=None, redoc_url=None) or protect with auth. Export the OpenAPI schema for client generation: app.openapi() returns the JSON schema.'

Complete FastAPI Rules Template

Consolidated rules for FastAPI projects.

  • Pydantic models for all I/O: separate Create, Update, Response models — never dict
  • response_model on every endpoint — filters output, auto-documents in OpenAPI
  • Depends() for DB sessions, auth, services — never global state or imports
  • async def for all I/O endpoints — async SQLAlchemy 2.0 with AsyncSession
  • HTTPException for expected errors — proper status codes, never error in 200
  • SQLAlchemy 2.0: select() not query(), Mapped types, DeclarativeBase
  • OpenAPI: tags for grouping, docstrings for descriptions, response models for errors
  • pytest + httpx AsyncClient — factory_boy for test data — pytest-asyncio for async tests