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.
Source
View on GitHubFastAPI
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 deffor I/O-bound work: HTTP calls, DB calls (with async drivers), file I/O.defis 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()insideasync defblocks the event loop. Either usehttpx.AsyncClientor move the call to adefroute.
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 vField()for constraints โge,le,min_length,pattern. No need for custom validators when a constraint suffices.- Separate
UserCreate/UserRead/UserUpdateโ a singleUserschema means you leak hashed passwords or accept fields users shouldn't set. model_config = ConfigDict(from_attributes=True)to read from ORM objects (wasorm_modein 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_useritself depends onget_db. - Use
Dependsin 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) meansxruns 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.environdirectly โBaseSettingsvalidates 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 userTesting
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'TestClientfor sync test code (most pytest setups).httpx.AsyncClient(app=app)for async tests.- Override
Dependsin tests withapp.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 defeverywhere reflexively โ costs you readability without throughput gains when the work is CPU-bound.exception_handlersthat swallow everything โ they should narrow on type, not catchException.- Reading
request.headers['Authorization']directly โ useDepends(oauth2_scheme)so OpenAPI docs reflect the auth scheme.