FastAPI 2 - De Basics
Na de FastAPI en Pydantic introductie uit de vorige les gaan we verder met de FastAPI tutorial.
Herhaling
- FastAPI (en Pydantic) werken op basis van type annotations.
- Gebruikt decorators om path functies te bepalen (zoals Flask).
- Automatische data validatie en conversie (coercion).
- Automatisch gegenereerde OpenAPI Spec and Swagger UI.
uv add fastapi[standard],uv run fastapi dev
Path Parameters
Query Parameters
@app.get("/items/")
def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]Een default waarde maakt de parameter optioneel in de OAS.
Pydantic & Request Bodies
- Data validatie en serialisatie (= omzetten van Python naar bvb. JSON).
ModelenField.- Gebruik van
Annotated.
main.py
from datetime import datetime
from typing import Annotated
from fastapi import FastAPI
from pydantic import UUID4, BaseModel, ConfigDict, Field, PositiveInt
from pydantic.alias_generators import to_camel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
class LineItem(BaseModel):
item: Item
quantity: PositiveInt
class Order(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)
order_date: datetime
customer_uuid: Annotated[ UUID4, Field(examples=["d4a93ebc-65b2-48e0-a804-310347129f07"])]
line_items: list[LineItem]
app = FastAPI()
@app.post("/orders/{order_type}")
def create_item(order_type: str, order: Order):
print(f"Received order of type {order_type}: {order}")
return orderQuery Parameter Validatie
https://fastapi.tiangolo.com/tutorial/query-params-str-validations/
Query()
Een query parameter is op zich altijd een gewone string (een substring van de volledige URL). Met een Python base type annotatie (int, bool, …) krijgen we wel conversie, maar voor extra validatie kunnen we geen volledig Pydantic model als type toekennen.
We zouden (via Annotated) gebruik kunnen maken van pydantic.Field. FastAPI voorziet echter specifiek voor dit geval zijn eigen fastapi.Query functie. Deze werkt op precies dezelfde manier als pydantic.Field, maar met extra functionaliteit m.b.t. query parameters.
In het volgende voorbeeld wordt de (optionele!) query parameter q beperkt tot 5 karakters.
Extra Parameters
De Query functie biedt nog tal van andere handige parameters. We bekijken er nog enkele in het volgende voorbeeld.
main.py
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
def read_items(
query: Annotated[
str | None,
Query(
1 description="Only return items matching this query.",
2 alias="q",
3 deprecated=True,
min_length=3,
),
] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if query:
results.update({"q": query})
return results- 1
-
Met
descriptionkan een beschrijving worden meegegeven. Deze wordt overgenomen in de OAS en getoond in de Swagger UI. - 2
-
qis geen goede naam voor een Python variable. We gebruiken liever bvb.query. Maar in de API spec willen we wel graagqblijven gebruiken. Dat kan metalias. - 3
-
Als we een parameter op termijn willen verwijderen uit de API kan deze als
deprecatedworden aangegeven. Opnieuw wordt dit overgenomen in de OAS en duidelijk weergegeven in de Swagger UI.
Custom Validatie
Net als bij een volledige Pydantic model kan het zijn dat we nog extra validatie willen uitvoeren op een query parameter. Net als een Pydantic model Field kunnen we gebruik maken van pydantic.AfterValidator, met een functie die als argument (en return value) de desbetreffende query parameter krijgt.
main.py
from typing import Annotated
from fastapi import FastAPI, HTTPException, status
from pydantic import AfterValidator
app = FastAPI()
data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}
def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id
@app.get("/items/")
def read_items(
id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
)
return {"id": id, "name": item}Path Parameter Validatie
fastapi.Path is het equivalent van fastapi.Query maar dan voor path parameters.
Query Parameter Modellen
Een groep gerelateerde query parameters kunnen we eenmalig definiëren als een Pydantic model. FastAPI zal dan automatisch elke veld (field) van het model als een afzonderlijke query parameter beschouwen. Wel moet de type hint in de path operation functie geannoteerd worden met een (eventueel lege) Query functie. FastAPI kan anders immers niet bepalen of het om een Query of Request Body gaat.
main.py
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_queryRequest Headers
Request headers maak je beschikbaar in de path operation functie via functie argumenten in combinatie met de Header functie. Die laatste werkt op dezelfde manier als bvb. Query of Path.
main.py
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
def read_items(
custom_header: Annotated[str, Header()],
user_agent: Annotated[str | None, Header(include_in_schema=False)] = None,
):
return {"User-Agent": user_agent, "Custom-Header": custom_header}HTTP headers gebruiken doorgaans een koppelteken (hyphen, -) tussen de woorden, zoals bvb. User-Agent. Maar dit teken mag niet gebruikt worden in Python namen (bvb. variabelen). FastAPI zet deze daarom automatisch om in een liggend streepje (underscore, _).
Een header als User-Agent verwachten we eigenlijk altijd en willend we bijgevolg niet opnemen in de OAS. Dat kan opnieuw met de parameter include_in_schema=False.
Net als bij query parameters kan een groep headers gedefinieerd worden als een Pydantic model en zal FastAPI deze automatisch als afzonderlijke header velden interpreteren.
main.py
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
def read_items(headers: Annotated[CommonHeaders, Header()]):
return headersRespons Model
https://fastapi.tiangolo.com/tutorial/response-model/
Return Value Type Hint
Tot nu toe keken we vooral naar het modelleren van de request. Met FastAPI (en Pydantic) kunnen we ook op dezelfde manier de respons definiëren, m.b.v. een type hint voor de functie return value!
main.py
FastAPI gebruikt de type hint om de OAS te vervolledigen. De type-checker van je editor zal ook controleren of we daadwerkelijk een Item object gereturned wordt.
Response_model
Het kan gebeuren dat de data waarover we beschikken in de functie (nog) niet overeenkomt met het type dat we in de respons willen. FastAPI kan de juiste conversie automatisch uitvoeren. I.p.v. een type hint voor de return value (die dus niet meer zou kloppen) gebruiken we dan de response_model parameter in de path operation decorator.
main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: list[str] = []
@app.post("/items/")
def create_item(item: Item) -> Item:
return item
@app.get("/items/", response_model=list[Item])
def read_items():
return [
{"name": "Portal Gun", "price": 42.0},
{"name": "Plumbus", "price": 32.0},
]De read_items returned dus een list van dict’s maar FastAPI zet dit om in een list van Item’s (die om hun beurt dan weer worden omgezet naar de juiste JSON representatie in de response body).
Response_model als Filter
FastAPI zal de return value van de functie ook, in de mate van het mogelijke, aanpassen aan het gedefinieerd response_model. Op die manier kunnen we specifieke data uitfilteren en hebben we zekerheid over de inhoud van de respons.
In onderstaand voorbeeld zorgen we er bvb. voor dat het password veld zeker niet wordt meegestuurd in de respons.
main.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
@app.post("/user/", response_model=UserOut)
def create_user(user: UserIn):
return userEr bestaat een beter manier om tot hetzelfde resultaat te komen. Een manier waarmee we wel nog type-validation in editors als VSCode of PyCharm behouden (nu is dat niet het geval want de return type van de functie is onbekend).
UserIn is eigenlijk een uitbreiding van UserOut. Het is UserOut + een extra password veld. Dit kan natuurlijk uitgedrukt wordt via een subclass.
Nu kan de return type van de functie als UserOut worden gedefinieerd. Ook al wordt er eigenlijk een UserIn gereturned. Immers, een UserIn is een UserOut!
main.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserIn(UserOut):
password: str
@app.post("/user/", response_model=UserOut)
def create_user(user: UserIn) -> UserOut:
return userZie ook https://fastapi.tiangolo.com/tutorial/extra-models/#reduce-duplication
Response_model_exclude
Er zijn een aantal response_model_exclude* parameters (met overeenkomstige Pydantic BaseModel.dict parameters) waarmee we de mogelijkheid krijgen bepaalde velden uit het response model uit te sluiten in de uiteindelijke response.
response_model_exclude_unsetzorgt dat enkel velden die gespecificeerd werden bij het aanmaken van het object (m.a.w. die in de input zaten) worden teruggestuurdresponse_model_exclude_defaultssluit alle default waarden uitresponse_model_exclude_nonesluit alle velden met waardeNone(null in JSON) uitresponse_model_excludeis eensetmet veldnamen (alsstr) die uitgesloten moeten worden
main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float = 10.5
tags: list[str] = []
items = {
"foo": Item(name="Foo", price=50.2),
"bar": Item(name="Bar", price=62, description="The bartenders", tax=20.2),
"baz": Item(name="Baz", price=50.2, description=None, tax=10.5, tags=[]),
}
@app.get("/read_item_exclude_unset/{item_id}", response_model_exclude_unset=True)
def read_item_exclude_unset(item_id: str) -> Item:
return items[item_id]
@app.get("/read_item_exclude_defaults/{item_id}", response_model_exclude_defaults=True)
def read_item_exclude_defaults(item_id: str) -> Item:
return items[item_id]
@app.get("/read_item_exclude_none/{item_id}", response_model_exclude_none=True)
def read_item_exclude_none(item_id: str) -> Item:
return items[item_id]
@app.get("/read_item_exclude_tax/{item_id}", response_model_exclude={"tax"})
def read_item_exclude_tax(item_id: str) -> Item:
return items[item_id]Geen Model
Het is voor FastAPI zeker niet verplicht om een Pydantic model als response type te hebben. Als de structuur van een respons niet vooraf gedefinieerd kan worden kan bvb. een gewone dict door de path operation functie gereturned worden.
Respons Status Code
https://fastapi.tiangolo.com/tutorial/response-status-code/#changing-the-default
Naast response_model kan ook status_code worden meegegeven in de path operation decorator. Hiermee wordt aangegeven wat de normale HTTP status code (default 200) is bij een succesvolle respons.
I.p.v. een int kan ook gebruik worden gemaakt van de handige fastapi.status module.
main.py
Ook hier wordt de toegevoegde informatie doorgevoerd in de OAS.
Forms
Als Parameters
Data uit Form velden wordt beschikbaar via functie argumenten geannoteerd met Form.
Form Models
Net als bij een JSON request Body kan opnieuw gebruik worden gemaakt van een Pydantic model om verschillende Form velden eenmalig samen te definiëren.
Error Responses
Om een fout (bvb. HTTP 404) terug te sturen naar de client kan een HTTPException exception geraised worden.
Deze heeft een verplicht status_code argument en optionele argumenten als detail. Omdat het een exception is kan dit rechtstreeks gebruikt worden in path operation functie, maar ook in onderliggende helper functies. We zagen hiervan al een voorbeeld in Custom Validatie.
main.py
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
def read_item(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
)
return {"item": items[item_id]}Merk op dat FastAPI (nog) geen automatisch oplossing heeft om de extra 404 respons op te nemen in het OAS schema. Als we dit willen moet het handmatig gedefinieerd worden via de responses parameter in de path decorator.
main.py
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
class ErrorMessage(BaseModel):
detail: str
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get(
"/items/{item_id}",
responses={
status.HTTP_404_NOT_FOUND: {
"description": "Item not found",
"model": ErrorMessage,
}
},
)
def read_item(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
)
return {"item": items[item_id]}Extra Decorator Parameters
We zagen hierboven reeds the responses parameters. Er zijn nog een aantal andere handige parameters die in de decorator gebruikt kunnen worden.
status_codevoor de status code van een normale succesvolle respons.tagsom operaties te groeperen.summaryom een korte beschrijving van de operatie te geven.descriptionom een uitgebreidere beschrijving te geven - maar dit kan beter via de functie doc-string.response_descriptionvoor de normale respons te beschrijven (i.p.v. default Successful response”).deprecatedom aan te geven dat de operatie niet meer gebruikt moet worden.
main.py
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/users/", summary="Get all users", tags=["users"])
async def read_users():
"""Get the list of all users:
By default returns only the username field.
"""
return [{"username": "johndoe"}]
@app.get("/items/", tags=["items"], response_description="A list of items")
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get(
"/elements/",
tags=["items"],
deprecated=True,
status_code=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION,
)
async def read_elements():
return [{"item_id": "Foo"}]JSON Encoder
Wanneer een path operation functie een Pydantic model returnt zorgt FastAPI automatisch voor een correcte omzetting naar JSON in de HTTP respons.
Diezelfde omzetting maakt FastAPI ook beschikbaar via de fastapi.encoders.jsonable_encoder functie. Dit kan erg handig zijn om een model naar een JSON-encodeerbare dict om te zetten. Bijvoorbeeld voor opslag in een JSON database zoals MongoDB.
In het volgende voorbeeld bevat het Pydantic model een datetime veld. Dit kan niet rechtstreeks worden omgezet naar JSON. De jsonable_encoder functie zal hier een ISO 8601 string van maken.
main.py
from datetime import datetime
from fastapi import FastAPI, HTTPException, status
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
fake_db = {}
class Item(BaseModel):
name: str
timestamp: datetime
description: str | None = None
app = FastAPI()
@app.post("/items/", response_model=Item)
def create_item(item: Item):
json_compatible_item_data = jsonable_encoder(item)
fake_db[item.name.lower()] = json_compatible_item_data
print(f"DEBUG: fake_db = {fake_db}")
return item
@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: str):
if item := fake_db.get(item_id):
return item
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
)Updates en Partial Updates
Met de HTTP POST methode worden nieuwe API resources (zoals in het vorige voorbeeld: Items) aangemaakt. Een bestaande resource te updaten kan op twee verschillende manieren, met twee verschillende HTTP methods: PUT en PATCH.
We bekijken eerst PUT door het vorige voorbeeld iets uit te breiden. Met PUT wordt het volledige Item vervangen door nieuwe data. Probeer aan de hand van volgende code te bepalen welk probleem kan voorkomen bij het gebruik van PUT.
main.py
from datetime import datetime
from fastapi import FastAPI, HTTPException, status
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
fake_db = {
"foo": {"name": "Foo", "timestamp": "2026-01-23T07:30:39.105Z", "price": 50.2},
"bar": {
"name": "Bar",
"timestamp": "2026-01-23T07:32:29.421Z",
"description": "The bartenders",
"price": 62,
"tax": 20.2,
},
"baz": {
"name": "Baz",
"timestamp": "2026-01-23T07:33:57.632Z",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": [],
},
}
class Item(BaseModel):
name: str
timestamp: datetime = datetime.fromtimestamp(0)
description: str | None = None
price: float | None = None
tax: float = 10.5
tags: list[str] = []
app = FastAPI()
@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: str):
if item := fake_db.get(item_id):
return Item(**item)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
)
@app.put("/items/{item_id}", response_model=Item)
def update_item(item_id: str, item: Item):
update_item_encoded = jsonable_encoder(item)
fake_db[item_id] = update_item_encoded
return update_item_encodedHet probleem met PUT zoals hier geïmplementeerd is dat default waarden (bvb tax) ongewild de bestaande waarde van een Item kunnen overschrijven.
In een PATCH request worden in principe enkel de gewijzigde velden meegegeven. Pydantic voorziet twee handige functies waar we gebruik van kunnen maken om dit gedrag met FastAPI te implementeren.
- Met
model_dump(exclude_unset=True)krijgen we van een bestaand model object eendictdie enkel de velden bevat die expliciet werden meegegeven bij het aanmaken van het object. Op die manier krijgen kunnen we dus enkel de velden uit de PATCH request verkrijgen. - Met
model_copy(update=update_data)krijgen we een kopie van een model object maar worden de velden uit deupdate_data(dict) gebruikt om de bestaande waarden te overschrijven.
main.py
@app.patch("/items/{item_id}", response_model=Item)
async def update_item_partial(item_id: str, item: Item):
stored_item_data = fake_db[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.model_dump(exclude_unset=True)
updated_item = stored_item_model.model_copy(update=update_data)
fake_db[item_id] = jsonable_encoder(updated_item)
return updated_itemOefening
Als oefening maken we een eenvoudige JSON API voor een ToDo/Task-management applicatie. Elke stap bouwt voort op de vorige. Probeer enkel gebruik te maken van de uitstekende FastAPI en Pydantic documentatie.
Een Task stelt een stuk werk voor, zoals:
- “Documentatie schrijven”
- “Bug fixen”
- “Presentatie voorbereiden”
Elke task heeft:
- Een ID
- Een titel met minimumlengte van 3 karakters
- Een optionele beschrijving
- Een prioriteit van 1 tot 5
- Een interne timestamp (
created_at) die automatisch wordt ingesteld- Is enkel voor intern gebruik en wordt nooit teruggestuurd naar de client.
Stap 1 – Task aanmaken & opvragen
Maak een eerste werkende API met volgende mogelijkheden:
- Tasks aanmaken (
POST /tasks)- De aangemaakte task wordt in de respons body teruggestuurd met status code 201.
- Één task opvragen (
GET /tasks/{task_id})
Gebruik voor de ‘opslag’ van de tasks een globale dictionary als pseudo-database.
Om een task voor te stellen zul je 3 modellen nodig hebben en gebruik maken van overerving.
Bvb:
TaskCreate:title,description,priorityTaskExternal:TaskCreate+task_idTaskInternal:TaskExternal+created_at
Oplossing
main.py
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Path, status
from pydantic import BaseModel, Field
from typing_extensions import Annotated
app = FastAPI()
TASKS_DB: dict[int, "TaskInternal"] = {}
class TaskCreate(BaseModel):
title: Annotated[str, Field(min_length=3)]
description: str | None = None
priority: Annotated[int, Field(ge=1, le=5)]
class TaskExternal(TaskCreate):
task_id: Annotated[int, Field(serialization_alias="id")]
class TaskInternal(TaskExternal):
created_at: Annotated[
datetime, Field(default_factory=lambda: datetime.now(tz=timezone.utc))
]
@app.post("/tasks", response_model=TaskExternal, status_code=status.HTTP_201_CREATED)
def create_task(task: TaskCreate):
next_task_id = max(TASKS_DB.keys(), default=0) + 1
new_task = TaskInternal(task_id=next_task_id, **task.model_dump())
TASKS_DB[next_task_id] = new_task
return new_task
@app.get("/tasks/{task_id}", response_model=TaskExternal)
def get_task(task_id: Annotated[int, Path(ge=1)]):
try:
return TASKS_DB[task_id]
except KeyError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")Stap 2 - Tasks filteren en updaten
Breid de code uit de vorige stap uit met twee extra API paths:
- Tasks oplijsten en filteren (
GET /tasks)- Stuurt by default alle tasks terug in een lijst
- Query parameter
min_priority(int) beperkt de lijst tot tasks met ten minste deze priority - Query parameter
q(str) beperkt de lijst tot tasks waarbij deze string in de titel voorkomt (substring match)
- Task aanpassen (
PATCH /tasks/{task_id})- Past een of meerdere van de volgende velden aan:
title,description,priority(partiële update) - Stuurt aangepaste task terug
- Past een of meerdere van de volgende velden aan:
Om een task aan de passen met PATCH zal een extra Pydantic model nodig zijn waarbij alle velden optioneel zijn.
Oplossing
main.py
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Path, Query, status
from pydantic import BaseModel, Field
from typing_extensions import Annotated
app = FastAPI()
TASKS_DB: dict[int, "TaskInternal"] = {}
class TaskCreate(BaseModel):
title: Annotated[str, Field(min_length=3)]
description: str | None = None
priority: Annotated[int, Field(ge=1, le=5)]
class TaskExternal(TaskCreate):
task_id: Annotated[int, Field(serialization_alias="id")]
class TaskInternal(TaskExternal):
created_at: Annotated[
datetime, Field(default_factory=lambda: datetime.now(tz=timezone.utc))
]
class TaskUpdate(TaskCreate):
title: Annotated[str | None, Field(min_length=3)] = None
priority: Annotated[int | None, Field(ge=1, le=5)] = None
TaskId = Annotated[int, Path(ge=1)]
@app.post("/tasks", response_model=TaskExternal, status_code=status.HTTP_201_CREATED)
def create_task(task: TaskCreate):
next_task_id = max(TASKS_DB.keys(), default=0) + 1
new_task = TaskInternal(task_id=next_task_id, **task.model_dump())
TASKS_DB[next_task_id] = new_task
return new_task
@app.get("/tasks/{task_id}", response_model=TaskExternal)
def get_task(task_id: TaskId):
try:
return TASKS_DB[task_id]
except KeyError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
@app.get("/tasks", response_model=list[TaskExternal])
def list_tasks(
min_priority: Annotated[int | None, Query(ge=1)] = None,
query: Annotated[str | None, Query(alias="q", min_length=3)] = None,
):
results = TASKS_DB.values()
if min_priority is not None:
results = [t for t in results if t.priority >= min_priority]
if query:
results = [t for t in results if query.lower() in t.title.lower()]
return results
@app.patch("/tasks/{task_id}", response_model=TaskExternal)
def update_task(task_id: TaskId, task_data: TaskUpdate):
stored_task = TASKS_DB.get(task_id)
if not stored_task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
update_data = task_data.model_dump(exclude_unset=True)
updated_task = stored_task.model_copy(update=update_data)
TASKS_DB[task_id] = updated_task
return updated_taskTesten
Met onderstaande code kan je zelf je API code testen. Daarvoor moet wel eerst pytest geïnstalleerd worden:
Om te testen maken we gebruiken we hier de FastAPI TestClient. Het gebruik is heel gelijkaardig aan de Flask TestClient.
Er wordt van uitgegaan dat in dezelfde directory een main module (main.py) bestaat met daarin een FastAPI app object. Om de testen uit te voeren:
De volledige code voor de oefening en testen staan ook hier: https://github.com/dvx76/fastapi-tutorial/tree/main/les2/oefening1.
test.py
test.py
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client():
"""Recreate app for every test to start with clean database."""
import importlib
import main
importlib.reload(main)
from main import app
return TestClient(app)
@pytest.mark.parametrize("priority", [1, 2, 3, 4, 5])
def test_post_task_all_priorities(client: TestClient, priority: int):
response = client.post(
"/tasks",
json={
"title": "abc",
"description": "test description",
"priority": priority,
},
)
assert response.status_code == 201
assert response.json() == {
"title": "abc",
"description": "test description",
"priority": priority,
"id": 1,
}
def test_post_task_no_description_and_id_increments(client: TestClient):
response_1 = client.post("/tasks", json={"title": "title 1", "priority": 1})
response_2 = client.post("/tasks", json={"title": "title 2", "priority": 1})
assert response_2.status_code == 201
assert response_1.json()["id"] == 1
assert response_2.json()["id"] == 2
@pytest.mark.parametrize(
"title, priority",
[("", 1), ("te", 1), ("test", -1), ("test", 0), ("test", 6), ("test", "a")],
)
def test_post_task_validation_errors(client: TestClient, title: str, priority: int):
response = client.post(
"/tasks",
json={
"title": title,
"priority": priority,
},
)
assert response.status_code == 422
def test_get_task_succeess(client: TestClient):
post_input = {
"title": "test title",
"description": "test description",
"priority": 1,
}
client.post("/tasks", json=post_input)
response = client.get("/tasks/1")
assert response.status_code == 200
expected_response = post_input | {"id": 1}
assert response.json() == expected_response
def test_get_task_not_found(client: TestClient):
response = client.get("/tasks/1")
assert response.status_code == 404
assert response.json()
def test_get_all_tasks_filtered(client: TestClient):
for post in range(1, 4):
client.post(
"/tasks",
json={
"title": f"test title {post}",
"description": f"test description {post}",
"priority": post,
},
)
# All tasks
response = client.get("/tasks")
assert response.status_code == 200
assert len(response.json()) == 3
# Only 2 tasks with priority >= 2
response = client.get("/tasks", params={"min_priority": 2})
assert response.status_code == 200
assert len(response.json()) == 2
# Only 1 task with priority >= 2 and 'title 2' in title
response = client.get("/tasks", params={"min_priority": 2, "q": "title 2"})
assert response.status_code == 200
assert len(response.json()) == 1
# Test min_priority read from cookies
response = client.get("/tasks", cookies={"min_priority": "3"})
assert response.status_code == 200
assert len(response.json()) == 1
# Test query param overrides preference from cookies
response = client.get(
"/tasks", params={"min_priority": 2}, cookies={"min_priority": "3"}
)
assert response.status_code == 200
assert len(response.json()) == 2
def test_patch_task(client: TestClient):
client.post(
"/tasks",
json={
"title": "test title",
"description": "test description",
"priority": 3,
},
)
response = client.patch("/tasks/1", json={"description": "updated"})
assert response.status_code == 200
assert response.json() == {
"title": "test title",
"description": "updated",
"priority": 3,
"id": 1,
}
def test_patch_task_found(client: TestClient):
response = client.patch("/tasks/1", json={"description": "updated"})
assert response.status_code == 404
def test_preferences_set(client: TestClient):
response = client.post("/preferences", json={"min_priority": 3})
assert response.status_code == 204
assert response.cookies == {"min_priority": "3"}Links
Wil je verder leren over FastAPI en Pydantic, bekijk dan zeker eens volgende links: