FastAPI basics
Hook
FastAPI turns a Python function into a JSON HTTP endpoint with a decorator and a type hint. You don’t write the JSON serialization, you don’t write the validation, you don’t write the docs. It’s the lowest-friction way to wrap a database in an API.
Concept
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/lions/seasons")def seasons() -> dict: return {"seasons": [2021, 2022, 2023, 2024]}Run it:
uvicorn module:app --reloadYou get:
GET /api/lions/seasonsreturning JSON- Auto-generated docs at
/docs(Swagger) and/redoc - Type-checked request and response with pydantic
- Hot reload on file change
Add query parameters with type hints:
@app.get("/api/lions/weekly-scoring")def weekly_scoring(season: int = 2024) -> dict: ...FastAPI parses ?season=2024, validates that it’s an integer, and rejects
?season=foo with a clean 422 error — for free.
Lions example
The actual L5 endpoint serving the chart at app.1pride.app:
from fastapi import FastAPI, Queryfrom sqlalchemy import textfrom .db import engine
app = FastAPI()
# "Scoring" is per-game, so this pulls from `schedules` even though the# endpoint name might suggest `weekly_stats` (which is per-player-week).@app.get("/api/lions/weekly-scoring")def weekly_scoring(season: int = Query(2024, ge=2000, le=2030)) -> dict: with engine().connect() as c: rows = c.execute(text(""" SELECT week, CASE WHEN home_team = 'DET' THEN away_team ELSE home_team END AS opp, CASE WHEN home_team = 'DET' THEN home_score ELSE away_score END AS scored, CASE WHEN home_team = 'DET' THEN away_score ELSE home_score END AS allowed FROM schedules WHERE season = :season AND game_type = 'REG' AND (home_team = 'DET' OR away_team = 'DET') ORDER BY week """), {"season": season}).mappings().all() return {"season": season, "games": [dict(r) for r in rows]}Three habits:
Query(..., ge=..., le=...)— bound your inputs. If someone hits?season=999, you’d rather return a clean 422 than execute the SQL.text(...)with:bindparams, not f-strings. SQL injection matters even for a public read-only API..mappings().all()— returns dict-shaped rows instead of tuples, serializes to JSON cleanly.
Try it
Add a /api/lions/qb-weekly endpoint that takes a season query parameter
(default 2024) and returns Jared Goff’s per-week passing yards from
weekly_stats. Bind the season; don’t string-format it.
Hit it locally:
curl 'http://localhost:8000/api/lions/qb-weekly?season=2023'Verify Swagger docs at http://localhost:8000/docs show the endpoint.
Common mistakes
- String-formatted SQL. Even on a read-only public API, treat parameter binding as non-negotiable.
- No bounds on numeric params.
?season=99999will hit the DB and return an empty result, but it’s cheap noise. Reject early. - Returning ORM objects directly. FastAPI handles dicts and pydantic
models cleanly; SQLAlchemy
Rowobjects need conversion. - Forgetting to close connections. Use a context manager (
with engine().connect() as c:) so connections return to the pool.
Quick check
- What’s the difference between
season: int = 2024andseason: int = Query(2024, ge=2000, le=2030)? - Why bind SQL parameters instead of f-string formatting?
- What does
/docsgive you, and when would you use it?