Pular para o conteúdo

Projeto: API web com Axum em Rust

Neste projeto você vai construir um servidor web de verdade: uma pequena API de tarefas. Outros programas (um site, um app de celular ou simplesmente o curl no seu terminal) enviam requisições HTTP para ela, e ela responde com JSON. É o mesmo tipo de coisa que faz funcionar o “back end” da maioria dos apps que você usa.

Vamos usar o axum, um framework web popular e amigável, feito pelo time do Tokio. Ele funciona em cima do Rust assíncrono e do serde, então este projeto junta muita coisa que você já aprendeu: structs, Result, smart pointers (ponteiros inteligentes) e estado compartilhado entre threads. Se você fez a Lista de tarefas, vai reconhecer a ideia. Desta vez, a lista de tarefas mora atrás de um endereço web.

Quando terminar, o seu servidor vai responder a estas requisições:

Método e caminho O que faz Status de sucesso
GET /todos Lista todas as tarefas 200 OK
GET /todos/{id} Busca uma tarefa 200 OK, ou 404 Not Found
POST /todos Cria uma tarefa a partir de JSON 201 Created
DELETE /todos/{id} Apaga uma tarefa 204 No Content, ou 404 Not Found

Em JSON, uma tarefa fica assim:

{"id":1,"title":"Comprar leite","done":false}

Um aviso importante: um servidor web fica esperando requisições para sempre e precisa de uma porta de rede, então ele não roda no Rust Playground. O código do servidor nesta página é para o seu próprio computador, com cargo run. Cada passo abaixo foi compilado e executado, e as respostas mostradas são reais. Onde uma parte do programa consegue rodar no Playground, você vai encontrar um botão “Experimente você mesmo”.

Crie um projeto novo e adicione quatro crates (pacotes):

Terminal window
cargo new todo-api
cd todo-api
cargo add [email protected] tokio --features tokio/full
cargo add serde --features derive
cargo add serde_json

Agora o seu Cargo.toml tem algo assim (os números de versão podem ser mais novos):

[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

O que cada um faz:

  • O axum cuida do HTTP: ele lê as requisições e envia as respostas.
  • O tokio é o runtime assíncrono que faz tudo rodar. Veja Async/Await.
  • O serde e o serde_json transformam suas structs em JSON e de volta. Veja JSON com Serde.

Você também vai precisar do curl para enviar requisições. Ele já vem no macOS, no Linux e no Windows 10 ou mais novo. (No Windows, digite curl.exe no PowerShell, porque lá o curl sozinho significa outra coisa.)

Troque o conteúdo de src/main.rs por isto:

use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Servidor ouvindo em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Olá, mundo!"
}

Três peças trabalham juntas aqui:

  • Um handler (tratador) é uma async fn que responde a uma requisição. O hello retorna um texto, e o axum o envia de volta como resposta.
  • O router (roteador) liga caminhos a handlers. .route("/", get(hello)) quer dizer “quando alguém enviar uma requisição GET para /, chame hello”.
  • O listener abre a porta 3000 no seu próprio computador (127.0.0.1), e o axum::serve começa a responder às requisições. Ele roda até você pará-lo.

Rode com cargo run. O programa imprime uma linha e fica esperando:

Servidor ouvindo em http://127.0.0.1:3000

Deixe-o rodando, abra um segundo terminal e envie uma requisição:

Terminal window
curl http://127.0.0.1:3000/
Olá, mundo!

O servidor não coloca uma quebra de linha depois do texto, então o prompt do seu terminal pode aparecer logo depois de Olá, mundo!. Isso é normal. Nesta página, cada resposta aparece numa linha própria. Você também pode abrir http://127.0.0.1:3000/ no navegador. Aperte Ctrl + C no primeiro terminal para parar o servidor. Você precisa parar e reiniciar o servidor toda vez que mudar o código.

APIs costumam responder com JSON. No axum, você embrulha qualquer valor que implemente o Serialize do serde em Json(...), e o axum o transforma em JSON e define o cabeçalho content-type certo:

use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/example", get(example_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Servidor ouvindo em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Olá, mundo!"
}
async fn example_todo() -> Json<Todo> {
let todo = Todo {
id: 1,
title: String::from("Aprender axum"),
done: false,
};
Json(todo)
}

A opção -i diz ao curl para mostrar também a linha de status e os cabeçalhos:

Terminal window
curl -i http://127.0.0.1:3000/example
HTTP/1.1 200 OK
content-type: application/json
content-length: 45
{"id":1,"title":"Aprender axum","done":false}

(O curl também imprime um cabeçalho date, que deixamos de fora nesta página.) As chaves do JSON são exatamente os nomes dos campos da struct. O content-length é o tamanho do corpo em bytes. Você pode conferir esse formato de JSON no Playground, sem servidor, porque é serde puro. Esta também é a struct NewTodo que vamos usar no passo 4 para os dados que chegam:

use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Debug, Deserialize)]
struct NewTodo {
title: String,
}
fn main() {
let todo = Todo { id: 1, title: String::from("Aprender axum"), done: false };
println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Comprar leite"), done: true }];
println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Passear com o cachorro"}"#).unwrap();
println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Passear com o cachorro"}"#);
println!("{}", bad.unwrap_err());
}

Saída:

{"id":1,"title":"Aprender axum","done":false}
[{"id":1,"title":"Aprender axum","done":false},{"id":2,"title":"Comprar leite","done":true}]
NewTodo { title: "Passear com o cachorro" }
missing field `title` at line 1 column 33
▶ Experimente você mesmo

Um Vec<Todo> vira um array JSON. É exatamente isso que o GET /todos vai retornar. A última linha é a mensagem de erro do serde_json, em inglês: “missing field title” quer dizer que falta o campo title.

Num caminho como /todos/2, o 2 muda de uma requisição para outra. No axum 0.8, você marca essa parte da rota com chaves, {id}, e lê o valor no handler com o extractor (extrator) Path:

use axum::{extract::Path, routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/hello/{name}", get(hello_name))
.route("/add/{a}/{b}", get(add));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Servidor ouvindo em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello_name(Path(name): Path<String>) -> String {
format!("Olá, {name}!")
}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String {
format!("{a} + {b} = {}", a + b)
}

Um extractor é um parâmetro do handler que tira um pedaço da requisição. Path(name): Path<String> é um pattern (padrão): ele desembrulha o Path e entrega a String que está dentro. Com dois parâmetros, você recebe uma tupla.

Terminal window
curl http://127.0.0.1:3000/hello/Ana
curl http://127.0.0.1:3000/add/2/3
Olá, Ana!
2 + 3 = 5

Agora, uma surpresa boa. Você disse que a e b são i32, então o axum confere isso por você. Envie algo que não é número:

Terminal window
curl -i http://127.0.0.1:3000/add/2/x
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`

O axum responde em inglês: a URL é inválida porque o valor x não pode ser convertido em i32. O seu handler nem chegou a rodar. Os tipos na assinatura da função são a validação.

Para criar uma tarefa, o cliente envia JSON no corpo (body) da requisição. O extractor Json lê o corpo e o desserializa na sua struct. O cliente só envia um título, então usamos uma struct separada, NewTodo, para a entrada:

use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/echo", post(echo_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Servidor ouvindo em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn echo_todo(Json(input): Json<NewTodo>) -> Json<Todo> {
let todo = Todo {
id: 1,
title: input.title,
done: false,
};
Json(todo)
}

Use post(...) em vez de get(...) na rota. No curl, -X POST define o método, -H adiciona um cabeçalho e -d envia o corpo:

Terminal window
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Comprar leite"}'
{"id":1,"title":"Comprar leite","done":false}

De novo, o axum confere a entrada antes de o seu código rodar. Se faltar o title no JSON, você recebe 422 Unprocessable Entity:

Terminal window
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Comprar leite"}'
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 24

A mensagem diz que não foi possível desserializar o corpo JSON no tipo esperado, porque falta o campo title. E se você esquecer o cabeçalho Content-Type, recebe 415 Unsupported Media Type com a mensagem Expected request with `Content-Type: application/json` . Esse cabeçalho é o que as pessoas mais esquecem ao testar com o curl.

Passo 5: Estado compartilhado para uma API de tarefas de verdade

Seção intitulada “Passo 5: Estado compartilhado para uma API de tarefas de verdade”

Até aqui, cada handler inventava os próprios dados. Uma API de verdade precisa lembrar das tarefas entre uma requisição e outra. As requisições podem chegar ao mesmo tempo e são tratadas em threads diferentes, então a lista precisa ser segura para compartilhar. Esse é o trabalho da dupla que você conheceu na página de Concorrência:

  • Mutex<Vec<Todo>> garante que só uma requisição altere a lista de cada vez.
  • Arc<...> permite que vários handlers tenham um ponteiro para essa mesma lista (veja Ponteiros inteligentes).

Damos um nome curto ao tipo com type Db = Arc<Mutex<Vec<Todo>>>;, entregamos ao router com .with_state(db) e lemos nos handlers com o extractor State. O axum clona o Arc (barato: ele só copia um ponteiro) para cada requisição.

Aqui está o programa completo. Troque o conteúdo de src/main.rs por ele:

use std::sync::{Arc, Mutex};
use axum::{
extract::{Path, State},
http::StatusCode,
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
// O "banco de dados": uma lista de tarefas que toda requisição compartilha.
type Db = Arc<Mutex<Vec<Todo>>>;
#[tokio::main]
async fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.with_state(db);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Servidor ouvindo em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
// GET /todos
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
let todos = db.lock().unwrap();
Json(todos.clone())
}
// GET /todos/{id}
async fn get_todo(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Todo>, StatusCode> {
let todos = db.lock().unwrap();
match todos.iter().find(|todo| todo.id == id) {
Some(todo) => Ok(Json(todo.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
// POST /todos
async fn create_todo(State(db): State<Db>, Json(input): Json<NewTodo>) -> (StatusCode, Json<Todo>) {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo {
id,
title: input.title,
done: false,
};
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
// DELETE /todos/{id}
async fn delete_todo(State(db): State<Db>, Path(id): Path<u32>) -> StatusCode {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
if todos.len() < before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}

Vamos ver as ideias novas:

  • Um caminho, vários métodos. get(list_todos).post(create_todo) manda GET /todos e POST /todos para handlers diferentes.
  • Códigos de status são valores de retorno. StatusCode vem do módulo http do axum. Um handler pode retornar um Result: Ok(Json(todo)) vira 200 com JSON, e Err(StatusCode::NOT_FOUND) vira um 404 vazio. Uma tupla (StatusCode::CREATED, Json(todo)) define o status e o corpo. Retornar só StatusCode::NO_CONTENT (204) quer dizer “feito, nada para devolver”.
  • O lock() e o guard. db.lock().unwrap() espera até nenhuma outra requisição estar usando a lista e então entrega um guard (guarda), que você pode ler e alterar como um Vec normal. A trava é liberada quando o guard sai do escopo, no fim da função.
  • Ids novos. O próximo id é o maior id da lista mais um, ou 1 se a lista estiver vazia.
  • Apagando. O retain mantém toda tarefa cujo id não bate. Se o tamanho não mudou, nada foi apagado, então retornamos 404.

Por que std::sync::Mutex e não um mutex assíncrono? Ele funciona bem (e é mais rápido), desde que você não segure a trava durante um .await. Nenhum dos nossos handlers faz isso: eles travam, trabalham com a lista e retornam.

A lógica da lista e da trava não precisa de servidor web, então você pode testá-la no Playground. Estas funções fazem o mesmo trabalho que os handlers, sem o axum:

use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
struct Todo {
id: u32,
title: String,
done: bool,
}
type Db = Arc<Mutex<Vec<Todo>>>;
fn create(db: &Db, title: &str) -> Todo {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo { id, title: title.to_string(), done: false };
todos.push(todo.clone());
todo
}
fn get(db: &Db, id: u32) -> Option<Todo> {
let todos = db.lock().unwrap();
todos.iter().find(|todo| todo.id == id).cloned()
}
fn delete(db: &Db, id: u32) -> bool {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
todos.len() < before
}
fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let second_handle = Arc::clone(&db); // o que o axum faz a cada requisição
println!("{:?}", create(&db, "Comprar leite"));
println!("{:?}", create(&second_handle, "Aprender axum"));
println!("{:?}", get(&db, 2));
println!("{:?}", get(&db, 99));
println!("apagou a 1? {}", delete(&db, 1));
println!("apagou a 1 de novo? {}", delete(&db, 1));
println!("restam: {}", db.lock().unwrap().len());
}

Saída:

Todo { id: 1, title: "Comprar leite", done: false }
Todo { id: 2, title: "Aprender axum", done: false }
Some(Todo { id: 2, title: "Aprender axum", done: false })
None
apagou a 1? true
apagou a 1 de novo? false
restam: 1
▶ Experimente você mesmo

A tarefa criada por meio do second_handle aparece quando você lê por meio do db, porque os dois Arcs apontam para a mesma lista.

Inicie o servidor com cargo run e, num segundo terminal, teste cada endpoint. No começo, a lista está vazia:

Terminal window
curl http://127.0.0.1:3000/todos
[]

Crie uma tarefa. Repare no status 201 Created:

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Comprar leite"}'
HTTP/1.1 201 Created
content-type: application/json
content-length: 45
{"id":1,"title":"Comprar leite","done":false}

Crie uma segunda e liste todas:

Terminal window
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Aprender axum"}'
curl http://127.0.0.1:3000/todos
{"id":2,"title":"Aprender axum","done":false}
[{"id":1,"title":"Comprar leite","done":false},{"id":2,"title":"Aprender axum","done":false}]

Busque uma pelo id e peça uma que não existe:

Terminal window
curl http://127.0.0.1:3000/todos/2
curl -i http://127.0.0.1:3000/todos/99
{"id":2,"title":"Aprender axum","done":false}
HTTP/1.1 404 Not Found
content-length: 0

Apague a tarefa 1 e depois tente apagá-la de novo:

Terminal window
curl -i -X DELETE http://127.0.0.1:3000/todos/1
curl -i -X DELETE http://127.0.0.1:3000/todos/1
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
content-length: 0
Terminal window
curl http://127.0.0.1:3000/todos
[{"id":2,"title":"Aprender axum","done":false}]

O axum também trata alguns erros por você. Um tipo errado no JSON dá um 422 que explica o problema, e um método que você não configurou para um caminho dá 405 Method Not Allowed, com um cabeçalho listando os métodos que funcionam:

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title": 42}'
curl -i -X PUT http://127.0.0.1:3000/todos/2
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12
HTTP/1.1 405 Method Not Allowed
allow: GET,HEAD,DELETE
content-length: 0

A mensagem do 422 diz que o title veio como número inteiro (42), mas era esperada uma string. Pare o servidor com Ctrl + C e inicie de novo: a lista está vazia. Os dados vivem só na memória. Salvá-los é a primeira ideia mais abaixo.

Usar a sintaxe antiga :id nas rotas. Muitos tutoriais na internet foram escritos para o axum 0.7, que usava /todos/:id. No axum 0.8 isso compila, mas o programa dá panic assim que começa:

thread 'main' (12345) panicked at src/main.rs:32:10:
Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.

A mensagem diz que segmentos de caminho não podem começar com : e que, para capturar valores, você deve usar {capture}. A correção: escreva /todos/{id}.

Colocar o Json antes dos outros extractors. O corpo da requisição só pode ser lido uma vez, então o extractor que o lê (Json) precisa ser o último parâmetro. Se você escrever create_todo(Json(input): Json<NewTodo>, State(db): State<Db>), o erro não diz isso diretamente. As linhas importantes são:

error[E0277]: the trait bound `fn(Json<NewTodo>, State<...>) -> ... {create_todo}: Handler<_, ...>` is not satisfied
|
31 | .route("/todos", get(list_todos).post(create_todo))
| ---- ^^^^^^^^^^^ unsatisfied trait bound
|
= note: Consider using `#[axum::debug_handler]` to improve the error message

Quando você vir “Handler is not satisfied” (o trait Handler não é satisfeito), confira a ordem dos parâmetros e confira se todo tipo de retorno é algo que o axum consegue transformar numa resposta. A nota sugere usar #[axum::debug_handler] para ter uma mensagem de erro mais clara.

Esquecer o -H "Content-Type: application/json" no curl. O servidor responde 415. O cabeçalho é obrigatório para o extractor Json.

Esquecer de reiniciar. O servidor que está rodando é o programa antigo. Pare com Ctrl + C e rode cargo run de novo depois de cada mudança.

Quando a API básica estiver funcionando, aqui vão bons próximos passos. Tente um de cada vez.

  • Marcar uma tarefa como concluída. Adicione PATCH /todos/{id} com .patch(...) na segunda rota. Encontre a tarefa com iter_mut(), defina done = true e retorne a tarefa.
  • Validação. Um título vazio não deveria ser aceito. Um handler pode retornar Result<(StatusCode, Json<Todo>), (StatusCode, String)> e devolver Err((StatusCode::BAD_REQUEST, String::from("o título não pode ficar vazio"))). Use title.trim().is_empty() para conferir.
  • Ids que nunca se repetem. Com “maior id mais um”, apagar a tarefa mais nova deixa o id dela ser usado de novo. Em vez disso, guarde um contador separado no seu estado, por exemplo uma struct com todos: Vec<Todo> e next_id: u32 dentro do Mutex.
  • Persistência com um arquivo JSON. Derive Deserialize também em Todo. Na inicialização, leia todos.json com std::fs::read_to_string e serde_json::from_str (ou comece vazio se o arquivo não existir). Depois de cada mudança, grave a lista de volta com serde_json::to_string_pretty e std::fs::write. As páginas de Arquivos e de JSON com Serde têm tudo o que você precisa. Mais tarde, você pode trocar o arquivo por um banco de dados de verdade, como o SQLite.
  • Logging com tower-http. Rode cargo add tower-http --features trace e cargo add tracing-subscriber, e depois adicione uma camada de logging. Agora o servidor imprime uma linha para cada requisição, com o método, o caminho, o status e quanto tempo ela levou:
use tower_http::trace::TraceLayer;
// no começo do main:
tracing_subscriber::fmt()
.with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
.init();
// ao montar o router:
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.layer(TraceLayer::new_for_http())
.with_state(db);
  • Notas sobre deploy. O 127.0.0.1 só aceita requisições do seu próprio computador. Para aceitar requisições de outras máquinas (por exemplo, dentro do Docker ou num servidor), use 0.0.0.0 no bind. Muitos serviços de hospedagem dizem ao seu programa qual porta usar por meio de uma variável de ambiente PORT, que você pode ler com std::env::var (veja Ambiente e processos). Compile com cargo build --release para ter um servidor bem mais rápido. O resultado é um único arquivo em target/release/ que você pode copiar para um servidor Linux ou colocar numa imagem Docker pequena. Para um site público, coloque-o atrás de um proxy reverso (como o Caddy ou o nginx) que cuide do HTTPS.
  • Um handler é uma async fn. Um router liga um método e um caminho a um handler. O axum::serve faz tudo rodar num TcpListener do tokio.
  • Os extractors tiram dados da requisição: Path para partes da URL (/todos/{id}), Json para o corpo (sempre por último) e State para dados compartilhados.
  • Retorne Json(valor) para JSON, StatusCode para um status, uma tupla para definir os dois, ou um Result para escolher entre sucesso e um status de erro.
  • Arc<Mutex<Vec<Todo>>> compartilha uma única lista com segurança entre todas as requisições. Trave, use e deixe o guard ser descartado. Não segure a trava durante um .await.
  • Teste sua API com o curl: -i mostra o status, -X define o método, -H adiciona um cabeçalho e -d envia um corpo.

Teste o que aprendeu

1. No axum 0.8, como você escreve uma rota com um id no caminho?

2. Um handler retorna (StatusCode::CREATED, Json(todo)). O que o cliente recebe?

3. Por que a lista compartilhada fica embrulhada em Arc<Mutex<...>>?

4. Qual extractor precisa vir por último nos parâmetros de um handler?

5. Você faz um curl -X POST com JSON, mas esquece o cabeçalho Content-Type: application/json. O que o axum responde?

Pontuação: 0 / 5