Beetje Coderen

Les 3 · Module 1 · Async en Axum-basis

Routing en handlers

Structureer de routes van kassa-api: methodes, geneste routers, fallback, statuscodes en responses die je zelf samenstelt met IntoResponse.

⏱ 70 minuten leestijd + oefenen

Illustratie: een browser met pijlen naar meerdere servers, verzoeken die de juiste weg vinden

🎯 Na deze les kun je

  • routes met verschillende HTTP-methodes en paden definiëren
  • routers nesten en samenvoegen in modules
  • statuscodes, headers en een fallback-handler teruggeven
  • je eigen type IntoResponse laten implementeren

kassa-api krijgt nu echte resources: producten en bestellingen. In deze les leg je de routes vast en leer je hoe handlers verschillende antwoorden teruggeven. Data bewaar je nog even niet; dat komt in les 6.

Methodes op één pad

use axum::{routing::{get, post, put, delete}, Router};

let producten = Router::new()
    .route("/", get(lijst_producten).post(maak_product))
    .route("/{id}", get(haal_product).put(wijzig_product).delete(verwijder_product));

Een pad krijgt één route-aanroep; methodes koppel je met .get().post()…. Twee keer .route("/{id}", …) voor hetzelfde pad geeft een panic bij het opstarten (Overlapping method route).

Nesten en modules

// src/main.rs
mod routes;

let app = Router::new()
    .route("/health", get(|| async { "ok" }))
    .nest("/v1/producten", routes::producten::router())
    .nest("/v1/bestellingen", routes::bestellingen::router())
    .fallback(niet_gevonden);
// src/routes/producten.rs
pub fn router() -> Router {
    Router::new().route("/", get(lijst)).route("/{id}", get(haal))
}

Elke module levert een Router; main stelt ze samen. Zo blijft main.rs kort, ook als de service groeit.

Responses samenstellen

Alles wat IntoResponse is, mag een handler teruggeven. Handige vormen:

use axum::{http::{StatusCode, header}, response::{IntoResponse, Response}, Json};

async fn maak_product() -> impl IntoResponse {
    (StatusCode::CREATED, [(header::LOCATION, "/v1/producten/7")], Json(serde_json::json!({ "id": 7 })))
}

async fn verwijder_product() -> StatusCode { StatusCode::NO_CONTENT }

async fn niet_gevonden() -> (StatusCode, &'static str) { (StatusCode::NOT_FOUND, "Deze route bestaat niet") }

Een tupel (StatusCode, headers, body) wordt in die volgorde toegepast.

Een eigen response-type

#[derive(serde::Serialize)]
struct Product { id: u32, naam: String, prijs_centen: u32 }

enum ProductAntwoord { Gevonden(Product), Onbekend }

impl IntoResponse for ProductAntwoord {
    fn into_response(self) -> Response {
        match self {
            ProductAntwoord::Gevonden(p) => Json(p).into_response(),
            ProductAntwoord::Onbekend => (StatusCode::NOT_FOUND, "Product niet gevonden").into_response(),
        }
    }
}

Voorspel eerst: je hebt .route("/v1/producten/{id}", get(haal)) en .route("/v1/producten/nieuw", get(formulier)). Welke handler krijgt GET /v1/producten/nieuw?

Uitkomst

formulier. Axums router (matchit) geeft een vast segment voorrang boven een parameter, ongeacht de volgorde waarin je ze toevoegt. /v1/producten/12 gaat naar haal. Is id een u32, dan geeft /v1/producten/abc een 400, want het pad matcht wel maar de extractor faalt.

Methode niet toegestaan

Doe je PATCH /v1/producten/1 terwijl alleen GET/PUT/DELETE bestaan, dan geeft Axum zelf 405 Method Not Allowed met een Allow-header. De fallback is alleen voor paden die helemaal niet bestaan.

Typische fouten

  • Overlapping method route: hetzelfde pad twee keer met .route. Voeg de methodes samen.
  • nest met een pad dat eindigt op /: nest("/v1/producten/", …) is ongeldig; laat de slash weg.
  • Verschillende return-types in if-takken: gebruik .into_response() in elke tak en return Response, of een eigen enum.

Oefenen

  1. Maak de modules routes/producten.rs en routes/bestellingen.rs met lege handlers die tijdelijk StatusCode::NOT_IMPLEMENTED teruggeven.
  2. Laat POST /v1/producten 201 met een Location-header teruggeven.
  3. Voeg een fallback toe die JSON {"fout":"niet gevonden","pad":"..."} geeft (tip: extractor axum::http::Uri).
  4. Implementeer ProductAntwoord en laat GET /v1/producten/{id} voor id 1 een vast product en anders 404 geven.
Modelantwoord oefening 1
// src/routes/mod.rs
pub mod producten;
pub mod bestellingen;

// src/routes/bestellingen.rs
use axum::{http::StatusCode, routing::get, Router};
pub fn router() -> Router {
    Router::new().route("/", get(|| async { StatusCode::NOT_IMPLEMENTED }).post(|| async { StatusCode::NOT_IMPLEMENTED }))
}
Modelantwoord oefening 2

Zie maak_product in Responses samenstellen. Controleer met curl -i -X POST localhost:3000/v1/producten.

Modelantwoord oefening 3
async fn niet_gevonden(uri: axum::http::Uri) -> (StatusCode, Json<serde_json::Value>) {
    (StatusCode::NOT_FOUND, Json(serde_json::json!({ "fout": "niet gevonden", "pad": uri.path() })))
}
Modelantwoord oefening 4
async fn haal(Path(id): Path<u32>) -> ProductAntwoord {
    if id == 1 { ProductAntwoord::Gevonden(Product { id: 1, naam: "Koffie".into(), prijs_centen: 280 }) }
    else { ProductAntwoord::Onbekend }
}

Lokaal oefenen

Geen live lab. Test elke route met curl -i zodat je de statuscode en headers ziet.

Samenvatting

  • Eén route per pad, methodes samenvoegen; nest en modules voor structuur.
  • Responses: tupels met statuscode en headers, of een eigen IntoResponse.
  • Axum geeft zelf 405 en 400; de fallback vangt onbekende paden.
  • Volgende les: extractors voor pad, query en JSON.

📝 Quiz: Routing en handlers

5 vragen · 70% nodig om te slagen

1. Hoe koppel je GET en POST aan hetzelfde pad?
2. Welke handler krijgt /producten/nieuw met ook /producten/{id}?
3. Wat geeft Axum bij een bestaande route met een verkeerde methode?
4. Wat doet een tupel (StatusCode, headers, Json)?
5. Waarom een eigen IntoResponse-enum?