Practical outcome
You will define a trustworthy /questions contract that rejects unsafe input, returns predictable output and explains recoverable errors to clients.
Send three payloads through the same API contract
Every request is untrusted
The client may send missing fields, the wrong type, an empty question or a document scope it should not control. Validation converts unknown network data into a known application object—or stops the request before business logic runs.
Define the request model
01from pydantic import BaseModel, Field0203class QuestionRequest(BaseModel):04question: str = Field(min_length=3, max_length=2000)05conversation_id: str | None = None06stream: bool = False
FastAPI reads JSON into this model. If validation succeeds, the route receives a QuestionRequest. If it fails, FastAPI returns a structured validation error.
01from fastapi import FastAPI, status0203app = FastAPI()0405@app.post("/questions", status_code=status.HTTP_202_ACCEPTED)06def submit_question(payload: QuestionRequest):07return {08"request_id": "req_123",09"status": "accepted",10"question": payload.question,11}
Validate meaning, not only data type
A string containing spaces technically has the right type but no useful question.
01from pydantic import BaseModel, Field, field_validator0203class QuestionRequest(BaseModel):04question: str = Field(min_length=3, max_length=2000)05conversation_id: str | None = None0607@field_validator("question")08@classmethod09def question_must_have_content(cls, value: str) -> str:10cleaned = value.strip()11if not cleaned:12raise ValueError("question must contain visible text")13return cleaned
Validation should establish format and safe boundaries. Authorisation and domain decisions still belong in application services.
Control the response contract
01class Citation(BaseModel):02source_id: str03title: str04page: int | None = None0506class AnswerResponse(BaseModel):07request_id: str08answer: str09citations: list[Citation]10insufficient_evidence: bool1112@app.post("/answers", response_model=AnswerResponse)13def answer_question(payload: QuestionRequest):14return build_answer(payload)
A response model documents the output and prevents accidental fields from escaping. For example, a database object may include internal notes or provider payloads that clients should never receive.
Use status codes to communicate outcomes
| Status | Meaning in this project |
|---|---|
200 OK | Answer returned successfully |
201 Created | New conversation or document created |
202 Accepted | Long-running ingestion accepted |
400 Bad Request | Request is syntactically valid but violates a business rule |
401 Unauthorized | Authentication is missing or invalid |
403 Forbidden | Identity is known but lacks permission |
404 Not Found | Requested resource does not exist in the caller’s scope |
422 Unprocessable Content | Request validation failed |
429 Too Many Requests | Rate or cost limit exceeded |
503 Service Unavailable | Required provider is temporarily unavailable |
Return useful application errors
01from fastapi import HTTPException, status0203@app.get("/conversations/{conversation_id}")04def get_conversation(conversation_id: str):05conversation = repository.find(conversation_id)06if conversation is None:07raise HTTPException(08status_code=status.HTTP_404_NOT_FOUND,09detail={10"code": "conversation_not_found",11"message": "The conversation could not be found.",12},13)14return conversation
Give clients a stable error code for program logic and a safe message for people. Do not expose stack traces, SQL statements, model prompts or provider secrets.
Create a consistent error envelope
01{02"error": {03"code": "insufficient_evidence",04"message": "No authorised source can answer this question.",05"request_id": "req_123",06"retryable": false07}08}
The request ID connects the client’s error with server logs. retryable helps a client decide whether another attempt makes sense.
Real-time scenario: invalid AI question
Reject early, explain clearly
A mobile client sends a 12,000-character prompt and a field named access_group: finance. The Pydantic model rejects the oversized prompt. The server ignores caller-provided access claims and derives scope from authenticated identity. The API returns a clear validation error without calling retrieval or the LLM—saving cost and reducing risk.
Common mistakes
Common mistakes
- Accepting unrestricted dictionaries instead of explicit request models.
- Returning raw database or provider objects to the client.
- Catching every exception and converting it to
200 OK. - Sending internal exception messages and stack traces over the API.
- Treating client-provided roles or tenant IDs as authorised identity.
Interview bit
Pydantic validation versus business validation
Pydantic validates and parses the shape of incoming data: types, lengths and field rules. Business validation checks domain state, such as whether a conversation exists or a document is active. Authorisation checks whether this identity may perform the operation. I keep those concerns separate so errors and tests remain precise.
Key takeaways
Key takeaways
- Validate untrusted network input before expensive AI work begins.
- Request models define accepted input; response models define exposed output.
- Use status codes and stable error codes to help clients recover.
- Never leak internal exceptions or secret provider payloads.
- Authentication claims must come from a verified identity, not the request body.
Frequently asked questions
Why validate before calling an AI model? Early validation rejects unusable input before it consumes model capacity, external API calls or database work.
What is the difference between a request model and a response model? A request model validates incoming data. A response model controls and documents the shape returned to the client.
Should every application error return status 500? No. Use a status that represents validation, authentication, permission, a missing resource or an unexpected server failure.
Interactive Knowledge Check
Choose an answer, inspect the explanation and explain the idea in your own words.