Programming with AI
ENRU
7.3

APIs, schemas and validation

the boundary where everything is checked

What it is

An API is the set of addresses the world can talk to your server through. A schema is a description of what data comes in and goes out. Validation is the check that what arrived matches the schema.

For working with an AI this is the most important place in a backend: the boundary where data arrives from outside is the only place it can still be checked. Miss it here and wrong data spreads through the whole application.

What it looks like in code

from pydantic import BaseModel, Field


class SubIn(BaseModel):
    city: str = Field(min_length=2, max_length=50)


class SubOut(BaseModel):
    user_id: int
    cities: list[str]


@app.post("/subs/{user_id}", response_model=SubOut, status_code=201)
def add_sub(user_id: int, body: SubIn) -> SubOut:
    cities = SUBS.setdefault(user_id, [])
    if body.city in cities:
        raise HTTPException(status_code=409, detail="already subscribed")
    cities.append(body.city)
    return SubOut(user_id=user_id, cities=cities)

What happens on invalid data:

curl -X POST http://127.0.0.1:8765/subs/7 \
     -H "Content-Type: application/json" -d '{"city":"X"}'
measured
[422]
{"detail":[{"type":"string_too_short","loc":["body","city"],
 "msg":"String should have at least 2 characters",
 "input":"X","ctx":{"min_length":2}}]}

The handler never ran at all. The check fired before it, and the response describes exactly what is wrong and where.

Why this is the same idea as types

Go back to article 6.3: mypy catches errors before the run. A schema does the same thing at the server's boundary — it catches wrong data before the handler.

The difference is where the boundary runs:

ToolWhat it checksWhen
mypyyour code agrees with itselfbefore running
The request schemaoutside data matches expectationsbefore the handler
The response schemayou returned what you promisedbefore sending

Three different frontiers, one idea: describe the shape of the data once and let a machine do the checking.

What goes in a schema

Not only types but constraints:

class SubIn(BaseModel):
    city: str = Field(min_length=2, max_length=50)
    notify_at: int = Field(ge=0, le=23)          # an hour from 0 to 23
    email: EmailStr | None = None                 # the format is checked

Every constraint in a schema is a check you won't have to write by hand and the AI won't forget.

Addresses

Short rules that are enough:

RuleExample
Plural nouns/subs, /users
The action is the method, not the addressDELETE /subs/7, not /deleteSub
Nesting through the path/users/7/subs
Filters through the query string/subs?city=Oslo&limit=10

What to tell the AI

Prompt
Describe the request and response schemas for
POST /subs/{user_id}: city is a string of 2 to 50 characters,
the notification hour is a number from 0 to 23 and optional.
The response has user_id and the list of cities.
Don't write the handler code yet.

A schema as a way of stating a task is the same as a signature in article 6.5: shorter than words and checked by a machine.

Prompt
Check that no internal fields end up in response_model:
password hashes, service ids, internal flags. List what gets
sent to the client.
Prompt
Which constraints should this schema have so that malformed
data never reaches the handler? Constraints only, don't touch
the logic.

What to look for

Validation inside the handler. if len(city) < 2: raise ... works, but the checks smear across the code and some cases get forgotten. Checks belong in the schema.

A dict instead of a schema. def add_sub(body: dict) switches checking off entirely: anything at all can arrive.

No response_model. Whatever the handler returned gets sent.

A schema with no constraints. city: str with no length accepts both an empty string and a megabyte-long one.

Different schemas for the same thing. Three slightly different descriptions of a subscription in three places will drift apart at the first edit.

Cheat sheet

  • The server's boundary is the only place outside data can still be checked.

  • The schema describes the shape, validation checks it, the framework does that for you.

  • On invalid data the handler never runs at all.

  • response_model protects against accidentally returning extra fields.

  • Constraints (min_length, ge, le) are checks that won't be forgotten.

  • A schema in a prompt works like a signature: more precise than words.