Free lesson
Create a FastAPI application with path operations
You will build a FastAPI application from scratch with a `GenAIServiceAPI` class that registers path operations for a prompt management service. Implement GET /prompts to list prompts with query parameter filtering (limit, offset, tags), POST /prompts to create prompts with request body validation, GET /prompts/{prompt_id} with path parameter parsing, and DELETE /prompts/{prompt_id} returning 204 No Content. Configure uvicorn programmatically with reload for development.
~25 min read · Free to read — no subscription required.
Create path operations with GET, POST, PUT, DELETE and proper HTTP status codes
Introduction
When you wire a prompt-management service in front of an LLM and return 200 OK whether the prompt was created, missing, or rejected by validation, every client and caching proxy in the chain loses the ability to tell success from failure — clients retry destructively, gateways cache errors as if they were data, and on-call engineers chase phantom bugs that the status line should have surfaced in the first place. By the end of this lesson you will be able to register path operations with @app.get, @app.post, @app.put, and @app.delete, extract path and query parameters via type annotations, and return the correct 2xx success code or raise the correct 4xx error for every CRUD interaction in a FastAPI service.
Key Terminology
- Path operation — a Python function bound to one HTTP method and one URL pattern by a FastAPI decorator (
@app.get("/prompts/{prompt_id}")); FastAPI uses its signature as the single source of truth for parameter parsing, validation, and OpenAPI docs. - Path parameter — a typed segment of the URL captured by
{name}placeholders (e.g.prompt_id: str); FastAPI extracts and coerces it before your handler runs, so a non-coercible value short-circuits to422automatically. - Query parameter — a function argument that does NOT appear in the path string (e.g.
skip: int = 0); a default value makes it optional, andNoneplus| Nonemakes it nullable in the OpenAPI schema. - Request body model — a Pydantic
BaseModelargument (e.g.payload: PromptCreate) that FastAPI parses from JSON and validates field-by-field; rejection produces a structured422with no handler code executed. - Status code — the integer set via the decorator's
status_code=for success responses or viaraise HTTPException(status_code=…)for errors; it is the primary success/failure signal HTTP requires, not the body.
Concepts
Method-specific decorators encode intent
FastAPI exposes one decorator per HTTP method — @app.get, @app.post, @app.put, @app.delete — instead of a single route() with a method list. The decorator is not cosmetic: it controls which method the path responds to, sets the default success status code, and emits the matching OpenAPI operation entry. Mismatching the method to the semantics (e.g. mutating state in a GET) breaks HTTP caching and retries silently and never throws an error you can grep for.
Path operations resolve in declaration order
When a request arrives, FastAPI walks registered routes top to bottom and the first path+method match wins. Static segments must therefore be registered before parameterized ones — declaring @app.get("/prompts/{prompt_id}") before @app.get("/prompts/featured") makes featured get captured as a prompt_id and either fail validation or return the wrong record. The diagram below traces the full pipeline (see Code Walkthrough for the concrete handlers).
Status codes are the contract, not the body
Choose the success code by the operation's semantics: 200 OK for GET and PUT that return data, 201 Created for POST that produces a resource, 204 No Content for DELETE and any handler that intentionally returns nothing. Choose the error code by the failure kind: 404 for a missing resource (raise it explicitly — do not return None with 200), 422 for validation rejection (FastAPI emits this for you when Pydantic rejects input), 405 when the path exists but the method is not registered. Encoding success/failure in the JSON body while returning 200 breaks proxies, retries, and observability.
Pydantic models double as request and response schemas
A Pydantic BaseModel on a parameter is interpreted as the JSON body; the same kind of model on the return annotation is the response schema. Field constraints (min_length, max_length, ge, le, default values) are validated automatically — the handler never executes when input violates them — and the constraints flow straight into the OpenAPI spec, so consumers see the same rules the server enforces.
Code Walkthrough
The snippet below stitches all four concepts together — method-specific decorators, path + query parameter extraction, Pydantic request and response models, and per-method status codes — into a complete prompt-management CRUD surface that exercises every code path the goal requires.
Code snippet python
1from datetime import datetime, timezone 2 3from fastapi import FastAPI, HTTPException, status 4from pydantic import BaseModel, Field 5 6app = FastAPI(title="Prompt Management API", version="1.0.0") 7prompts_db: dict[str, dict] = {} 8 9class PromptCreate(BaseModel): 10 name: str = Field(min_length=1, max_length=100) 11 template: str = Field(min_length=10) 12 model_name: str = Field(default="claude-3-sonnet") 13 temperature: float = Field(default=0.7, ge=0.0, le=2.0) 14 15class PromptResponse(BaseModel): 16 prompt_id: str 17 name: str 18 template: str 19 model_name: str 20 temperature: float 21 created_at: str 22 23@app.get("/prompts", status_code=status.HTTP_200_OK) 24def list_prompts(skip: int = 0, limit: int = 20, model_name: str | None = None): 25 results = list(prompts_db.values()) 26 if model_name is not None: 27 results = [p for p in results if p["model_name"] == model_name] 28 return results[skip : skip + limit] 29 30@app.get("/prompts/{prompt_id}", status_code=status.HTTP_200_OK) 31def get_prompt(prompt_id: str) -> PromptResponse: 32 record = prompts_db.get(prompt_id) 33 if record is None: 34 raise HTTPException( 35 status_code=status.HTTP_404_NOT_FOUND, 36 detail=f"Prompt '{prompt_id}' not found", 37 ) 38 return PromptResponse(**record) 39 40@app.post("/prompts", status_code=status.HTTP_201_CREATED) 41def create_prompt(payload: PromptCreate) -> PromptResponse: 42 prompt_id = f"prompt_{len(prompts_db) + 1:04d}" 43 record = { 44 "prompt_id": prompt_id, 45 **payload.model_dump(), 46 "created_at": datetime.now(timezone.utc).isoformat(), 47 } 48 prompts_db[prompt_id] = record 49 return PromptResponse(**record) 50 51@app.put("/prompts/{prompt_id}", status_code=status.HTTP_200_OK) 52def update_prompt(prompt_id: str, payload: PromptCreate) -> PromptResponse: 53 existing = prompts_db.get(prompt_id) 54 if existing is None: 55 raise HTTPException( 56 status_code=status.HTTP_404_NOT_FOUND, 57 detail=f"Prompt '{prompt_id}' not found", 58 ) 59 updated = { 60 "prompt_id": prompt_id, 61 **payload.model_dump(), 62 "created_at": existing["created_at"], 63 } 64 prompts_db[prompt_id] = updated 65 return PromptResponse(**updated) 66 67@app.delete("/prompts/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT) 68def delete_prompt(prompt_id: str) -> None: 69 if prompts_db.pop(prompt_id, None) is None: 70 raise HTTPException( 71 status_code=status.HTTP_404_NOT_FOUND, 72 detail=f"Prompt '{prompt_id}' not found", 73 ) 74 return None
- Lines 9-13:
PromptCreateis the request body schema; theFieldconstraints fail invalid input with422before the handler runs. - Lines 15-21:
PromptResponseis the response schema; declaring it as thereturntype makes server-managed fields likeprompt_idandcreated_atpart of the published contract. - Lines 23-28:
list_promptsshows query parameters (skip,limit,model_name) — defaults make them optional, andstr | Noneplus a default ofNonemakes the filter nullable. - Lines 30-38:
get_promptuses a path parameter and raisesHTTPException(404)for the missing case — never returnNonewith200here. - Lines 40-49:
create_promptreturns201 Created(set viastatus_code=on the decorator) and lets Pydantic enforce every body constraint before the handler executes. - Lines 51-65:
update_promptis the idempotentPUT— it preservescreated_atso repeated identical calls converge to the same stored record and returns200, not201. - Lines 67-74:
delete_promptreturns204 No Content; FastAPI sends an empty body, which is the correct REST semantic for a successful delete.
You'll know it works when uvicorn main:app --reload boots cleanly and curl -i -X POST http://localhost:8000/prompts -H 'content-type: application/json' -d '{"name":"q","template":"hello world test"}' returns HTTP/1.1 201 Created, a follow-up GET /prompts/prompt_0001 returns 200, GET /prompts/missing returns 404, DELETE /prompts/prompt_0001 returns 204 with an empty body, and a POST with "template":"x" returns 422 with a structured detail array — and /docs shows all five operations grouped by path with the right method, status, and schema for each.
Do's and Don'ts
Do's
- ✓Do raise
HTTPExceptionwith an explicitstatus_code— returningNoneor an error dict with a200status hides the failure from clients, proxies, and dashboards that key off the status line. - ✓Do set
status_code=on every decorator —POSTshould default to201,DELETEto204, andPUT/GETto200; relying on the framework default (200) misrepresents creation and deletion. - ✓Do declare static paths before parameterized ones —
@app.get("/prompts/featured")must register before@app.get("/prompts/{prompt_id}")or the second route will swallowfeaturedas an ID.
Don'ts
- ✗Don't mutate state inside a
GET— caches, retries, and link prefetchers assumeGETis safe and idempotent; mutating inGETcauses silent duplicate writes. - ✗Don't fuzzy-match a missing resource to
200with an empty body — clients can't distinguish "not found" from "found but empty"; raise404so the contract is unambiguous. - ✗Don't parse and validate the request body by hand — typing the parameter as a Pydantic
BaseModelgives you422for free and keeps the OpenAPI schema in sync with the enforced rules.
Keep going with GenAI Agent Engineering
Create a free account to track your progress and open this lesson in the full learning view. Subscribe to unlock the entire path — every goal, the hands-on labs, quizzes, and your verifiable skill graph — from . Cancel anytime.