๐Ÿ”งAutoSkills
All items
skillv1.0.0

python-fastapi

FastAPI patterns: async, dependencies, Pydantic v2, project layout, error handling

fastapipythonbackendapi

Install

$npx autoskills --items python-fastapi

Or scan + install everything matching your stack with npx autoskills.

FastAPI

FastAPI looks like Flask in five minutes and burns you in five months if you don't internalize the async model and Pydantic v2 semantics.

Project layout

app/
  main.py             # FastAPI() + router include
  routers/
    users.py          # APIRouter() per resource
    posts.py
  schemas/            # Pydantic models (request/response)
    user.py
  models/             # SQLAlchemy / Tortoise / etc.
    user.py
  services/           # business logic โ€” pure functions over models
    user_service.py
  deps.py             # FastAPI dependencies (Depends)
  config.py           # pydantic-settings BaseSettings
  db.py               # session/connection
  • Routers per resource, not per HTTP method.
  • Schemas separate from models. ORM models leak DB shape into your API contract.
  • Services own business logic. Routers should be thin: parse input โ†’ call service โ†’ return response.

Async โ€” but only where it pays

  • async def for I/O-bound work: HTTP calls, DB calls (with async drivers), file I/O.
  • def is fine for CPU-bound or sync-only libs. FastAPI runs sync routes in a thread pool โ€” you don't lose throughput.
  • Don't mix sync DB in async routes โ€” requests.get() inside async def blocks the event loop. Either use httpx.AsyncClient or move the call to a def route.

Pydantic v2 essentials

from pydantic import BaseModel, Field, EmailStr, field_validator
 
class UserCreate(BaseModel):
    email: EmailStr
    name: str = Field(min_length=1, max_length=100)
    age: int = Field(ge=0, le=150)
 
    @field_validator('name')
    @classmethod
    def name_no_emojis(cls, v: str) -> str:
        if any(ord(c) > 0x1F000 for c in v):
            raise ValueError('emojis not allowed')
        return v
  • Field() for constraints โ€” ge, le, min_length, pattern. No need for custom validators when a constraint suffices.
  • Separate UserCreate / UserRead / UserUpdate โ€” a single User schema means you leak hashed passwords or accept fields users shouldn't set.
  • model_config = ConfigDict(from_attributes=True) to read from ORM objects (was orm_mode in v1).

Dependency injection

from fastapi import Depends
 
async def get_db() -> AsyncSession:
    async with SessionLocal() as session:
        yield session
 
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    user = await auth_service.user_from_token(token, db)
    if not user:
        raise HTTPException(401)
    return user
 
@router.get('/me')
async def me(user: User = Depends(get_current_user)):
    return user
  • Dependencies compose โ€” get_current_user itself depends on get_db.
  • Use Depends in the signature, not function-body calls. FastAPI can't infer the dependency tree from runtime calls.
  • Sub-dependency caching โ€” Depends(x, use_cache=True) (default) means x runs once per request even if depended on twice.

Error handling

from fastapi import HTTPException, status
 
raise HTTPException(
    status_code=status.HTTP_404_NOT_FOUND,
    detail='User not found',
)

For domain errors, register an exception handler:

class DomainError(Exception):
    def __init__(self, code: str, message: str):
        self.code = code; self.message = message
 
@app.exception_handler(DomainError)
async def domain_error_handler(request, exc: DomainError):
    return JSONResponse(
        status_code=400,
        content={'error': exc.code, 'message': exc.message},
    )

Don't try/except everywhere in routers โ€” let the framework's handler do it.

Configuration with pydantic-settings

from pydantic_settings import BaseSettings
 
class Settings(BaseSettings):
    database_url: str
    redis_url: str = 'redis://localhost'
    debug: bool = False
 
    model_config = {'env_file': '.env'}
 
settings = Settings()
  • Don't read os.environ directly โ€” BaseSettings validates types and gives you a single source.
  • settings = Settings() at module load is fine; FastAPI starts once.

Background tasks vs. queues

  • BackgroundTasks โ€” run after response, in the same process. Fine for: send-email-after-signup, log-event.
  • Celery / Arq / Dramatiq โ€” separate process, durable, retries. Use for: anything where dropping the task hurts.
@router.post('/signup')
async def signup(data: UserCreate, bg: BackgroundTasks):
    user = await user_service.create(data)
    bg.add_task(send_welcome_email, user.email)
    return user

Testing

from fastapi.testclient import TestClient
 
def test_signup(client: TestClient):
    r = client.post('/users', json={'email': 'a@b.com', 'name': 'A', 'age': 30})
    assert r.status_code == 201
    assert r.json()['email'] == 'a@b.com'
  • TestClient for sync test code (most pytest setups).
  • httpx.AsyncClient(app=app) for async tests.
  • Override Depends in tests with app.dependency_overrides[get_db] = lambda: test_session.

What to avoid

  • Putting business logic in routers โ€” they should orchestrate, not compute.
  • Returning ORM objects from routers without a response schema โ€” leaks fields, breaks contracts.
  • async def everywhere reflexively โ€” costs you readability without throughput gains when the work is CPU-bound.
  • exception_handlers that swallow everything โ€” they should narrow on type, not catch Exception.
  • Reading request.headers['Authorization'] directly โ€” use Depends(oauth2_scheme) so OpenAPI docs reflect the auth scheme.