Les 4 · Module 1 · Async en Axum-basis
Extractors: Path, Query en JSON
Haal gegevens uit verzoeken met extractors: padparameters, querystrings met optionele velden, JSON-bodies, headers, en wat er gebeurt als het misgaat.
🎯 Na deze les kun je
- Path, Query en Json gebruiken met eigen structs
- optionele queryparameters en standaardwaarden afhandelen
- de volgorde van extractors en de body-regel begrijpen
- de standaardfout bij een ongeldige body opvangen
Handlers krijgen hun invoer via extractors: argumenten van een type dat FromRequestParts of FromRequest implementeert. In
deze les gebruik je de belangrijkste om kassa-api producten te laten zoeken en aanmaken.
Path
async fn haal(Path(id): Path<u32>) -> ... { }
// meerdere parameters: tupel of struct
async fn regel(Path((bestelling, regel)): Path<(u32, u32)>) -> ... { } // /v1/bestellingen/{bestelling}/regels/{regel}
Query
#[derive(Deserialize)]
struct Zoek {
q: Option<String>,
#[serde(default = "standaard_pagina")]
pagina: u32,
max_prijs: Option<u32>,
}
fn standaard_pagina() -> u32 { 1 }
async fn lijst(Query(z): Query<Zoek>) -> String {
format!("zoek {:?}, pagina {}, max {:?}", z.q, z.pagina, z.max_prijs)
}
/v1/producten?q=thee&max_prijs=300 geeft zoek Some("thee"), pagina 1, max Some(300). Velden die niet in de query staan, moeten
Option zijn of een standaardwaarde hebben; anders geeft Axum een 400.
Json
#[derive(Deserialize)]
struct NieuwProduct { naam: String, prijs_centen: u32 }
async fn maak(Json(nieuw): Json<NieuwProduct>) -> (StatusCode, Json<Product>) {
let p = Product { id: 7, naam: nieuw.naam, prijs_centen: nieuw.prijs_centen };
(StatusCode::CREATED, Json(p))
}
Json<T> controleert de header Content-Type: application/json, leest de body en deserialiseert met Serde.
Voorspel eerst: welke statuscodes krijg je voor deze drie verzoeken?
curl -i -X POST localhost:3000/v1/producten -d '{"naam":"Thee","prijs_centen":250}'
curl -i -X POST localhost:3000/v1/producten -H 'Content-Type: application/json' -d '{"naam":"Thee"'
curl -i -X POST localhost:3000/v1/producten -H 'Content-Type: application/json' -d '{"naam":"Thee","prijs_centen":-1}'
Uitkomst
- 415 Unsupported Media Type:
curl -dstuurt standaardapplication/x-www-form-urlencoded. - 400 Bad Request: de JSON is syntactisch kapot (
Failed to parse the request body as JSON). - 422 Unprocessable Entity: geldige JSON, maar
-1past niet in eenu32(Failed to deserialize the JSON body into the target type).
Axum onderscheidt dus "geen JSON", "kapotte JSON" en "JSON met de verkeerde vorm".
Volgorde: de body als laatste
Een request-body kan maar één keer gelezen worden. Daarom mag maar één extractor de body consumeren (Json, String, Bytes,
Form), en die moet het laatste argument zijn:
async fn wijzig(Path(id): Path<u32>, headers: HeaderMap, Json(w): Json<NieuwProduct>) -> ... { } // goed
Zet je Json vóór Path, dan geeft de compiler de bekende Handler is not implemented-fout.
Fouten zelf afhandelen
Wil je de standaardfout vervangen, vraag dan Result<Json<T>, JsonRejection>:
use axum::extract::rejection::JsonRejection;
async fn maak(body: Result<Json<NieuwProduct>, JsonRejection>) -> Response {
match body {
Ok(Json(n)) => (StatusCode::CREATED, Json(n.naam)).into_response(),
Err(e) => (e.status(), Json(serde_json::json!({ "fout": e.body_text() }))).into_response(),
}
}
In les 8 maak je hier een nette, herbruikbare oplossing van.
Overige extractors
| Extractor | Geeft |
|---|---|
HeaderMap |
alle headers |
TypedHeader<UserAgent> (axum-extra) |
één header, getypt |
Method, Uri |
methode en URL |
String / Bytes |
ruwe body |
Form<T> |
formulierdata |
State<T> |
gedeelde state (les 6) |
Typische fouten
- Velden heten anders in JSON: Rust gebruikt
snake_case; zet#[serde(rename_all = "camelCase")]als je clientprijsCentenstuurt. - Twee body-extractors: compileert niet. Kies er één.
Query<u32>: een query is altijd een map; gebruik een struct.
Oefenen
- Maak
GET /v1/producten?q=&max_prijs=&pagina=en geef de geparste waarden terug als JSON. - Maak
POST /v1/productenmetNieuwProducten test de drie foutgevallen uit Voorspel eerst. - Laat onbekende velden in de body weigeren met
#[serde(deny_unknown_fields)]. Welke statuscode krijg je bij{"naam":"x","prijs_centen":1,"kleur":"rood"}? - Maak
PUT /v1/producten/{id}metPath, de headerIf-MatchuitHeaderMap, en eenJson-body.
Modelantwoord oefening 1
#[derive(Deserialize, Serialize)]
struct Zoek { q: Option<String>, max_prijs: Option<u32>, #[serde(default = "standaard_pagina")] pagina: u32 }
async fn lijst(Query(z): Query<Zoek>) -> Json<Zoek> { Json(z) }
Modelantwoord oefening 2
Zie Json. Met -H 'Content-Type: application/json' en geldige data: 201 met het product.
Modelantwoord oefening 3
422: de JSON is geldig, maar past niet bij het type (unknown field kleur). Handig om tikfouten van clients vroeg te zien.
Modelantwoord oefening 4
async fn wijzig(Path(id): Path<u32>, headers: HeaderMap, Json(w): Json<NieuwProduct>) -> impl IntoResponse {
let versie = headers.get(header::IF_MATCH).and_then(|v| v.to_str().ok()).unwrap_or("geen");
Json(serde_json::json!({ "id": id, "naam": w.naam, "if_match": versie }))
}
Lokaal oefenen
Geen live lab. Gebruik curl -i en let op de statuscodes 400, 415 en 422.
Samenvatting
Path,QueryenJsonzetten verzoekdata om naar je eigen typen.- Optionele queryvelden:
Optionof#[serde(default)]. - Eén body-extractor, als laatste argument.
- 415 geen JSON, 400 kapotte JSON, 422 verkeerde vorm; vang
JsonRejectionom zelf te antwoorden. - Volgende les: Serde in de diepte.