Wrapping Models in APIs
Explore how to wrap large language models in robust FastAPI endpoints that validate inputs, separate prompt logic from transport, support streaming responses, and handle errors safely. This lesson guides you to design maintainable, testable API contracts essential for deploying reliable AI services.
A stable API boundary turns an LLM pipeline into something other services can call without inheriting your internal prompt code. Start with the smallest FastAPI contract that validates input and returns a predictable JSON shape. Then widen the boundary to support streaming and failure mapping while keeping provider variability behind a service layer.
This lesson covers a single POST endpoint, a service function boundary, an event stream option, and an error model.
To ground the contract, examine the minimal FastAPI route below, which uses typed request and response models.
class CreateWidgetRequest(BaseModel):model_config = ConfigDict(extra="forbid")name: str = Field(min_length=1, max_length=60)quantity: int = Field(ge=1, le=1_000)note: str | None = Field(default=None, max_length=200)priority: int = Field(default=0, ge=0, le=10)class WidgetResponse(BaseModel):model_config = ConfigDict(extra="ignore")id: strname: strquantity: intnote: str | Nonepriority: intcreated_at: datetimeapp = FastAPI()@app.post("/widgets", response_model=WidgetResponse)def create_widget(payload: CreateWidgetRequest) -> WidgetResponse:return WidgetResponse(id=str(uuid4()),name=payload.name,quantity=payload.quantity,note=payload.note,priority=payload.priority,created_at=datetime.now(timezone.utc),)
Line 2 (
model_config = ConfigDict(extra="forbid")): Any request body containing an unlisted field is rejected outright instead of having the extra field silently dropped.Lines 4–7 (the
Field(...)constraints): Pydantic enforces each constraint (min_length,ge/le, etc.) beforecreate_widgetruns, so aquantityof 0 or anameof""never reaches the handler body.Line 11 (
model_config = ConfigDict(extra="ignore")): The opposite policy applies to responses: if the object being converted toWidgetResponsecarries extra attributes, they're dropped from the JSON instead of causing an error.Line 24 (
@app.post("/widgets", response_model=WidgetResponse)): Declares the endpoint's guarantee: every successful response matchesWidgetResponseexactly, and unlisted fields never leak into the JSON.Lines 25–33 (
create_widget): By the time this function's first line executes,payloadis already validated, so the function only constructs the response and never checks its own input.
Here's what that buys you locally, with no HTTP request involved:
CreateWidgetRequest.model_validate({"name": "", "quantity": 0, "extra": "nope"})# raises pydantic.ValidationError with three separate field failures:# name too short, quantity below its minimum, and "extra" not permitted
FastAPI validates the request body against the Pydantic model before your handler runs, so ...