Beetje Coderen

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.

⏱ 75 minuten leestijd + oefenen

Illustratie: een document waaruit pijlen naar dozen wijzen, gegevens die uit een verzoek gehaald worden

🎯 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
  1. 415 Unsupported Media Type: curl -d stuurt standaard application/x-www-form-urlencoded.
  2. 400 Bad Request: de JSON is syntactisch kapot (Failed to parse the request body as JSON).
  3. 422 Unprocessable Entity: geldige JSON, maar -1 past niet in een u32 (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 client prijsCenten stuurt.
  • Twee body-extractors: compileert niet. Kies er één.
  • Query<u32>: een query is altijd een map; gebruik een struct.

Oefenen

  1. Maak GET /v1/producten?q=&max_prijs=&pagina= en geef de geparste waarden terug als JSON.
  2. Maak POST /v1/producten met NieuwProduct en test de drie foutgevallen uit Voorspel eerst.
  3. Laat onbekende velden in de body weigeren met #[serde(deny_unknown_fields)]. Welke statuscode krijg je bij {"naam":"x","prijs_centen":1,"kleur":"rood"}?
  4. Maak PUT /v1/producten/{id} met Path, de header If-Match uit HeaderMap, en een Json-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, Query en Json zetten verzoekdata om naar je eigen typen.
  • Optionele queryvelden: Option of #[serde(default)].
  • Eén body-extractor, als laatste argument.
  • 415 geen JSON, 400 kapotte JSON, 422 verkeerde vorm; vang JsonRejection om zelf te antwoorden.
  • Volgende les: Serde in de diepte.

📝 Quiz: Extractors: Path, Query en JSON

5 vragen · 70% nodig om te slagen

1. Welke statuscode bij JSON zonder Content-Type: application/json?
2. Welke statuscode bij geldige JSON met een verkeerd type veld?
3. Waar moet de Json-extractor staan?
4. Wat gebeurt er als een verplicht queryveld ontbreekt?
5. Hoe vang je de JSON-fout zelf op?