Lezione 3 di 7 · 25 min di lettura
Strade e metodi
Un gestore che guarda la richiesta. Smistare per percorso con il pattern matching sulle liste, rispondere secondo il metodo, leggere la query, e provare il gestore con wisp/simulate, senza accendere nessun server.
Il percorso è una lista
Il gestore della lezione precedente rispondeva sempre la stessa cosa. Un sito vero risponde in modo diverso a /, a /books, a /books/42. Decidere quale codice deve rispondere a quale percorso si chiama routing (dall’inglese route, “strada”), e in Gleam non serve niente di speciale per farlo: basta un case.
wisp.path_segments(request) restituisce il percorso spezzato in una lista di stringhe, come request.path_segments della lezione 9.1. E sulle liste sai già fare pattern matching (lezione 3.2):
case wisp.path_segments(request) {
[] -> home(request)
["books"] -> books(request)
["books", id] -> book(request, id)
_ -> wisp.not_found()
}[]è il percorso vuoto, cioè/, la pagina principale.["books"]è esattamente/books.["books", id]è/books/seguito da un pezzo qualsiasi, che finisce nella variabileid:/books/42dàid = "42"._è tutto il resto, a cui rispondiamo404.wisp.not_found()è una risposta già pronta, con il codice404e il corpoNot found.
Il case controlla i rami in ordine, e il compilatore ti obbliga a gestire tutti i casi: un percorso che non corrisponde a nessuna strada non può restare senza risposta.
Il metodo
Allo stesso percorso possono arrivare metodi diversi. /books con GET vuol dire “dammi i libri”, con POST “aggiungi un libro”. Il metodo è il campo request.method, di tipo http.Method, e anche lui si guarda con un case:
case request.method {
Get -> list_books()
Post -> create_book(request)
_ -> wisp.method_not_allowed([Get, Post])
}wisp.method_not_allowed risponde 405, e aggiunge l’intestazione allow con l’elenco dei metodi permessi, come vuole il protocollo. Quando una strada accetta un solo metodo, c’è una scorciatoia, wisp.require_method, da usare con use (lezione 5.3):
fn home(request: Request) -> Response {
use <- wisp.require_method(request, Get)
wisp.html_response("<h1>The library</h1>", 200)
}Se il metodo è GET, require_method chiama il resto della funzione; altrimenti risponde 405 da solo, e il resto non viene mai eseguito. È lo stesso meccanismo di bool.guard: una funzione che riceve “il resto del codice” come ultimo argomento, e decide se chiamarlo.
Provare senza server
Per provare un gestore potresti accendere il server e fare un curl per ogni strada. Funziona, ma è lento: ogni modifica vuol dire spegnere, riaccendere, riscrivere le richieste. C’è un modo migliore, che viene dal fatto che il gestore è solo una funzione: basta chiamarla, con una richiesta finta.
Le richieste finte le costruisce il modulo wisp/simulate: simulate.request(metodo, percorso) crea una wisp.Request, e simulate.read_body(risposta) legge il corpo di una risposta come stringa. Ecco una piccola biblioteca, provata da main:
import gleam/http.{Get, Post}
import gleam/int
import gleam/io
import gleam/list
import wisp.{type Request, type Response}
import wisp/simulate
pub fn handle_request(request: Request) -> Response {
case wisp.path_segments(request) {
[] -> home(request)
["books"] -> books(request)
["books", id] -> book(request, id)
_ -> wisp.not_found()
}
}
fn home(request: Request) -> Response {
use <- wisp.require_method(request, Get)
wisp.html_response("<h1>The library</h1>", 200)
}
fn books(request: Request) -> Response {
case request.method {
Get -> wisp.ok() |> wisp.string_body("Dune, Emma, Ulysses")
Post -> wisp.created()
_ -> wisp.method_not_allowed([Get, Post])
}
}
fn book(request: Request, id: String) -> Response {
use <- wisp.require_method(request, Get)
case int.parse(id) {
Ok(number) ->
wisp.ok() |> wisp.string_body("Book number " <> int.to_string(number))
Error(_) -> wisp.bad_request("the id must be a number")
}
}
pub fn main() -> Nil {
[
simulate.request(Get, "/"),
simulate.request(Get, "/books"),
simulate.request(Post, "/books"),
simulate.request(http.Delete, "/books"),
simulate.request(Get, "/books/42"),
simulate.request(Get, "/books/dune"),
simulate.request(Get, "/shop"),
]
|> list.each(fn(request) {
let response = handle_request(request)
io.println(
http.method_to_string(request.method)
<> " "
<> request.path
<> " -> "
<> int.to_string(response.status)
<> " "
<> simulate.read_body(response),
)
})
}GET / -> 200 <h1>The library</h1>
GET /books -> 200 Dune, Emma, Ulysses
POST /books -> 201 Created
DELETE /books -> 405 Method not allowed
GET /books/42 -> 200 Book number 42
GET /books/dune -> 400 Bad request: the id must be a number
GET /shop -> 404 Not foundSette richieste, sette risposte, in un attimo e senza aprire nessuna porta. Qualche dettaglio:
import gleam/http.{Get, Post}importa due varianti dihttp.Methodsenza prefisso (lezione 4.5);Delete, usata una volta sola, restahttp.Delete. Il pacchettogleam_httpl’hai aggiunto nella lezione 9.1.import wisp.{type Request, type Response}fa lo stesso con i tipi, così le firme si leggono meglio:fn home(request: Request) -> Response./books/dunearriva alla strada giusta, ma"dune"non è un numero:wisp.bad_requestrisponde400, “hai sbagliato tu”. È il posto giusto per controllare i dati che arrivano da fuori, come i decoder della lezione 8.2.wisp.created()risponde201. Per ora non crea davvero niente: ci arriveremo.
Questo è anche il modo in cui si testano i server Gleam: gli esempi ufficiali di wisp hanno un file in test/ con una funzione per strada, che fa la richiesta finta e controlla la risposta con assert (lezione 5.5).
Accenderlo davvero
Il gestore di library_routes è pub, quindi un altro modulo può usarlo. Il server vero è un modulo a parte, con il main della lezione precedente:
import gleam/erlang/process
import library_routes
import mist
import wisp
import wisp/wisp_mist
pub fn main() -> Nil {
let assert Ok(_) =
wisp_mist.handler(library_routes.handle_request, wisp.random_string(64))
|> mist.new
|> mist.port(8000)
|> mist.start
process.sleep_forever()
}Avvialo con gleam run -m library_server, e prova dal secondo terminale:
curl localhost:8000/books/42
curl -i -X DELETE localhost:8000/booksBook number 42
HTTP/1.1 405 Method Not Allowed
allow: GET, POST
content-length: 18
date: Mon, 28 Sep 2026 13:53:04 GMT
connection: keep-alive
Method not allowedLe stesse risposte di main, perché è la stessa funzione. Nota l’intestazione allow: GET, POST messa da method_not_allowed. Separare così il gestore dall’avvio del server è la forma che ha quasi ogni progetto wisp.
La query
I parametri dopo il ? si leggono con wisp.get_query(request), che restituisce una lista di coppie #(nome, valore), già decodificate (%20 diventa uno spazio, per esempio). Per trovarne uno c’è list.key_find, che restituisce un Result:
import gleam/http.{Get}
import gleam/io
import gleam/list
import gleam/string
import wisp.{type Request, type Response}
import wisp/simulate
fn handle_request(request: Request) -> Response {
let name = case list.key_find(wisp.get_query(request), "name") {
Ok(name) -> name
Error(_) -> "stranger"
}
wisp.ok() |> wisp.string_body("Hello, " <> name <> "!")
}
pub fn main() -> Nil {
let response = handle_request(simulate.request(Get, "/hello?name=Ada"))
io.println(simulate.read_body(response))
let response = handle_request(simulate.request(Get, "/hello"))
io.println(simulate.read_body(response))
let query = wisp.get_query(simulate.request(Get, "/?a=1&b=two%20words&a=3"))
io.println(string.inspect(query))
}Hello, Ada!
Hello, stranger!
[#("a", "1"), #("b", "two words"), #("a", "3")]L’ultima riga mostra due cose da sapere: i valori arrivano già decodificati, e lo stesso nome può comparire più volte. list.key_find restituisce il primo; se ti servono tutti, c’è list.key_filter.
Quando usare la query e quando il percorso? Di solito il percorso dice quale cosa vuoi (/books/42), e la query come la vuoi: filtri, ordinamento, pagina (/books?sort=year&page=2).
Quiz
Una strada è scritta ["books", id]. Quale di questi percorsi NON ci arriva?
Esercizio · sul tuo computer
Il forno
Crea src/bakery.gleam, con un gestore per un forno che accetta solo richieste GET:
/rispondeWelcome to the bakery;/breadrisponde con i pani, separati da virgole:baguette, ciabatta, pretzel(tienili in unaconst);/bread/<nome>rispondeOne <nome>, coming up!se quel pane c’è, altrimenti404;- tutto il resto è
404.
Nel main, prova il gestore con simulate su GET /, GET /bread, GET /bread/pretzel, GET /bread/croissant e POST /bread, stampando per ognuna il codice di stato e il corpo:
200 Welcome to the bakery
200 baguette, ciabatta, pretzel
200 One pretzel, coming up!
404 Not found
405 Method not allowedSuggerimento: “solo GET” vale per tutto il forno, quindi require_method può stare in cima al gestore, prima del case.
Mostra una soluzione (prima prova da solo!)
import gleam/http.{Get, Post}
import gleam/int
import gleam/io
import gleam/list
import gleam/string
import wisp.{type Request, type Response}
import wisp/simulate
const breads = ["baguette", "ciabatta", "pretzel"]
pub fn handle_request(request: Request) -> Response {
use <- wisp.require_method(request, Get)
case wisp.path_segments(request) {
[] -> wisp.ok() |> wisp.string_body("Welcome to the bakery")
["bread"] -> wisp.ok() |> wisp.string_body(string.join(breads, ", "))
["bread", name] ->
case list.contains(breads, name) {
True -> wisp.ok() |> wisp.string_body("One " <> name <> ", coming up!")
False -> wisp.not_found()
}
_ -> wisp.not_found()
}
}
pub fn main() -> Nil {
[
simulate.request(Get, "/"),
simulate.request(Get, "/bread"),
simulate.request(Get, "/bread/pretzel"),
simulate.request(Get, "/bread/croissant"),
simulate.request(Post, "/bread"),
]
|> list.each(fn(request) {
let response = handle_request(request)
io.println(
int.to_string(response.status) <> " " <> simulate.read_body(response),
)
})
}Con require_method in cima, il POST /bread riceve 405 prima ancora di guardare il percorso. Se vuoi, avvia il forno per davvero con un modulo bakery_server, come library_server, e chiedi a curl localhost:8000/bread/ciabatta.
Ricapitolando
- Il routing è un
casesuwisp.path_segments(request):[]è/,["books", id]cattura un pezzo in una variabile,_è il404. - Il metodo è
request.method: uncaseconwisp.method_not_allowed([...])per i metodi che non accetti, oppureuse <- wisp.require_method(request, Get)per una strada con un solo metodo. - Risposte pronte:
wisp.ok()(200),wisp.created()(201),wisp.bad_request(dettaglio)(400),wisp.not_found()(404),wisp.method_not_allowed(405). Per il corpo,wisp.string_bodyewisp.html_response. - La query:
wisp.get_query(request)dà una lista di coppie;list.key_findne cerca una. wisp/simulatecrea richieste finte (simulate.request) e legge le risposte (simulate.read_body): il gestore si prova chiamandolo, senza server.- Gestore in un modulo, avvio del server in un altro.
Nella prossima lezione vedremo come avvolgere un gestore con dei middleware, per il log, gli errori e la sicurezza, e riceveremo i dati di un modulo HTML.