A support-ticket API accepts {"title": 7, "description": "<very large text>", "email": "alice@example.com"} without checking the request shape. A later formatting step crashes on the numeric title, while an error handler records the submitted description and email. The attacker needed no account and no exploit chain, only a request that crossed the API boundary with data the service treated as trustworthy. Our question is where the service should reject malformed, oversized, and unexpected input before it reaches business logic or logs.
We will keep one FastAPI service throughout the course. POST /tickets accepts a new ticket, GET /tickets/{id} reads one, POST /login authenticates a user, and GET /me returns the authenticated user. Each endpoint receives data controlled by a client, even when the client is a trusted browser today, so each one needs an explicit boundary between received input and internal state.
Input validation is the boundary rule that accepts only data matching the service’s contract. For POST /tickets, a valid request has a short text title, a bounded description, and an email in the required format. Malformed JSON fails even earlier because the framework cannot construct a request body from invalid syntax. The useful result is not merely fewer exceptions. Rejected input never reaches ticket storage, response rendering, or diagnostic logs that could expose personal data.
The service has three boundaries worth keeping visible as we build: request parsing, authentication at POST /login, and response rendering. The first boundary is our starting point because every unauthenticated request reaches it. Let’s trace the controlled fields through the service:
This diagram shows that title, description, and email arrive from outside the service, while the in-memory repository receives only data that passed the request boundary.
Because the diagram separates client data from repository data, POST /tickets needs rules that the handler can rely on rather than assumptions scattered through later code. The contract names permitted fields, their types, and their limits. A title might allow one through 120 characters, while a description might allow up to 4,000. Limits are security controls because they bound storage, processing, log volume, and response size before an attacker can amplify them.
Normalization belongs in the same boundary when the service needs one canonical form. We can trim surrounding whitespace from a title before storing it, but we should not silently repair an invalid email or truncate an oversized description. Repairing changes the client’s meaning; rejecting makes the contract observable. Pydantic field validators can transform accepted values, but validators must return the accepted result, and rules that need raw input must account for input of any type.
What should POST /tickets return when email is not-an-email? The handler should not create ticket 42, and the client needs a stable error shape rather than an internal traceback. The following request model applies bounded fields and records the rejected field without recording the rejected email itself: