RustArena

Step 2 of 7

Routing by method and path

A server receives every request in one place and must decide which code handles it. That's routing: look at the method and the path, and pick a handler. In Rust, a match on a tuple of the method and the path segments does it beautifully, thanks to slice patterns:

#[derive(Debug, Clone, Copy, PartialEq)]
enum Method {
    Get,
    Post,
    Delete,
}

fn route(method: Method, path: &str) -> (u16, String) {
    let path = path.split('?').next().unwrap_or("");
    let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
    match (method, segments.as_slice()) {
        (Method::Get, ["health"]) => (200, String::from("ok")),
        (Method::Get, ["players"]) => (200, String::from("list players")),
        (Method::Post, ["players"]) => (201, String::from("create a player")),
        (Method::Get, ["players", id]) => match id.parse::<u32>() {
            Ok(id) => (200, format!("show player {id}")),
            Err(_) => (400, format!("bad id {id:?}")),
        },
        (Method::Delete, ["players", id]) => (204, format!("delete player {id}")),
        (_, ["health"] | ["players"] | ["players", _]) => (405, String::from("method not allowed")),
        _ => (404, String::from("not found")),
    }
}

fn main() {
    let tries = [
        (Method::Get, "/players"),
        (Method::Get, "/players/42?fields=name"),
        (Method::Get, "/players/ferris"),
        (Method::Post, "/players/"),
        (Method::Delete, "/players/7"),
        (Method::Delete, "/health"),
        (Method::Get, "/teams"),
    ];
    for (method, path) in tries {
        let (status, body) = route(method, path);
        println!("{method:?} {path} -> {status} {body}");
    }
}
Get /players -> 200 list players
Get /players/42?fields=name -> 200 show player 42
Get /players/ferris -> 400 bad id "ferris"
Post /players/ -> 201 create a player
Delete /players/7 -> 204 delete player 7
Delete /health -> 405 method not allowed
Get /teams -> 404 not found

Piece by piece:

Common mistakes

Your task

Write the router for a books API. route(method, path) returns a Route: GET /books is ListBooks, POST /books is CreateBook, GET /books/{id} is GetBook(id), DELETE /books/{id} is DeleteBook(id), GET /books/{id}/reviews is ListReviews(id). An id that isn't a u32 gives BadRequest; one of those paths with another method gives MethodNotAllowed (whatever the id); everything else is NotFound. Ignore query strings and trailing slashes.

Challenge 2

Query strings carry options like filters and pages: /search?q=crab&page=2. Write fn parse_query(target: &str) -> (&str, BTreeMap<String, String>) returning the path and the parameters. Parameters are separated by &; each is key=value (split at the first =), and a + in a value means a space. A key without = maps to an empty string; empty pieces (a=1&&b=2) are skipped.

Challenge 3

List endpoints take paging options. Write fn page_params(query: &BTreeMap<String, String>) -> Result<(usize, usize), String> that reads page (default 1, must be at least 1) and per_page (default 20, must be 1 to 100) and returns (offset, limit) for a database query, where offset is (page - 1) * per_page. Errors: invalid page "x" and per_page must be 1 to 100.

Previous: HTTP in one page Next: JSON in, JSON out