Learn / Python and FastAPI Backend Engineering
Routing and Validation Basics
Design a reviewable FastAPI contract with typed create/read schemas, explicit status semantics, validation evidence, route tests, and an OpenAPI drift check.
Course: Python and FastAPI Backend Engineering. Level: Intermediate. Topic: Backend craft.
Stage: basic - Basic API contracts - FastAPI routing and validation contracts. Start by making every request, response, status code, and validation failure visible in the API contract.
Outcomes
- Model request bodies, response bodies, and server-owned fields as separate contracts.
- Return explicit status codes and client-safe validation behavior for success and failure paths.
- Prove the route contract with a focused test and an OpenAPI schema assertion.
Concepts
- APIRouter boundary
- Pydantic contract
- 422 validation response
- OpenAPI drift check
Concept flow
Show where FastAPI validates input, where service decisions happen, and how the same code becomes OpenAPI evidence.
- Request body
- Pydantic validation
- Validation error
- Service boundary
- Response model
- OpenAPI schema
Session flow
- Draw the API contract before the handler (concept, 10 min) — Separate client-owned input fields from server-owned output fields before implementing the route.
- A create schema should not accept id, timestamps, or internal status.
- A read schema should make the public response stable for clients and docs.
- Build the vertical route slice (walkthrough, 22 min) — Implement APIRouter, request model, response model, service dependency, and explicit success/conflict semantics together.
- The route should reveal the boundary: HTTP in, typed payload through, typed response out.
- Expected service decisions should map to client-safe HTTP errors.
- Prove validation and docs cannot drift silently (exercise, 14 min) — Write one HTTP test for invalid input and one OpenAPI assertion for request/response schemas.
- The 422 test protects client error handling.
- The OpenAPI assertion protects consumers who read the schema instead of your implementation.
Code example
Python in app/items/routes.py.
from typing import Annotated
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict, Field
router = APIRouter(prefix="/items", tags=["items"])
class ItemCreate(BaseModel):
name: str = Field(min_length=2, max_length=80)
quantity: int = Field(ge=0, le=500)
class ItemRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
name: str
quantity: int
class ItemService:
async def create(self, payload: ItemCreate) -> ItemRead:
if payload.name.lower() == "reserved":
raise ValueError("reserved-name")
return ItemRead(id=uuid4(), name=payload.name, quantity=payload.quantity)
async def get_item_service() -> ItemService:
return ItemService()
@router.post(
"",
response_model=ItemRead,
status_code=status.HTTP_201_CREATED,
responses={422: {"description": "Request body failed validation"}},
)
async def create_item(
payload: ItemCreate,
service: Annotated[ItemService, Depends(get_item_service)],
) -> ItemRead:
try:
return await service.create(payload)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Item name is reserved.",
) from exc
Walkthrough examples
- Contract pack for an item creation route — A product team is adding inventory intake. The route must be obvious to frontend developers, API consumers, and reviewers before it reaches production.
- File: app/items/routes.py
- File: app/items/service.py
- File: tests/test_items_contract.py
- File: docs/api/items-openapi-check.md
- Start with ItemCreate and ItemRead so ownership is visible before the handler body.
- Add a success test that asserts 201 and response fields, then an invalid-body test that asserts 422.
- Read app.openapi() in a test and assert the POST operation uses ItemCreate for the request and ItemRead for 201.
- Capture the invalid response and OpenAPI assertion output in the lesson review notes.
Practice
- Add one create route whose request model excludes server-owned fields like id and timestamps.
- Send one invalid body and save the 422 response shape as review evidence.
- Assert the OpenAPI schema still exposes ItemCreate for the request and ItemRead for the response.
Checklist
- Request and response models are separate and named by intent.
- The route declares status_code and response_model explicitly.
- Validation and conflict failures return predictable client-safe responses.
- Tests cover success, invalid input, and OpenAPI contract drift.
Quiz prompts
- A teammate adds id to ItemCreate so tests can reuse one model. What is the review concern? — Create models should describe what the client controls; read models can include server-owned fields such as id.
- Which test best proves validation is part of the public API contract? — A validation contract test should exercise the actual HTTP boundary and assert the shape clients depend on.
- Why add an OpenAPI schema assertion for this small route? — OpenAPI is a product surface for API consumers, so contract drift deserves a repeatable check.
- A reserved item name should not expose the internal ValueError string. Which response is better? — Validation errors, conflicts, and server faults communicate different ownership and retry behavior.
Flashcards
- Why split FastAPI create and read models? The create model describes client-owned input. The read model can include server-owned fields like id, timestamps, and computed status.
- What does a 422 response prove in this lesson? FastAPI rejected a syntactically valid request whose body failed the declared Pydantic field constraints.
- What is an OpenAPI drift check? A test that inspects the generated schema so accidental contract changes are caught before consumers see them.
- When is 409 better than 422? Use 409 when the request shape is valid but conflicts with a domain rule or current resource state.
- What does APIRouter buy you beyond shorter files? It groups a bounded API surface with prefix, tags, dependencies, and reviewable route ownership.
Labs
- Add a contract-tested item lookup endpoint — Extend the item API with GET /items/{item_id} while preserving clear request, response, error, and OpenAPI evidence.
- Add an ItemRead response model to GET /items/{item_id} without accepting server-owned fields from the client.
- Return 404 with a client-safe detail when the service cannot find the item.
- Add tests for success, missing item, invalid UUID/path behavior, and OpenAPI schema shape.
- Record the exact failing output you saw before the implementation passed.
- Tests prove 201 create, 422 invalid create, 200 lookup, and 404 missing item behavior.
- The OpenAPI assertion names the request schema and response schema used by the route.
- Review notes include at least one captured command output or schema excerpt as evidence.
Challenge
- Review-ready FastAPI contract slice (Core) — Turn the item route into a pull-request-sized API contract pack: route code, service seam, tests, OpenAPI check, and a short review note for client teams.
- Request and response schemas encode ownership clearly.
- Success, validation failure, conflict, and not-found behavior have explicit statuses.
- The OpenAPI schema check fails if the request or response model drifts.
- The review note includes command evidence and one tradeoff about status semantics.
Canonical lesson URL