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.
O que você vai construir
Seção intitulada “O que você vai construir”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”.
Preparando o projeto
Seção intitulada “Preparando o projeto”Crie um projeto novo e adicione quatro crates (pacotes):
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonAgora 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.)
Passo 1: Olá, mundo!
Seção intitulada “Passo 1: Olá, mundo!”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 fnque responde a uma requisição. Ohelloretorna 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çãoGETpara/, chamehello”. - O listener abre a porta 3000 no seu próprio computador (
127.0.0.1), e oaxum::servecomeç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:3000Deixe-o rodando, abra um segundo terminal e envie uma requisição:
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.
Passo 2: Retornar JSON a partir de uma struct
Seção intitulada “Passo 2: Retornar JSON a partir de uma struct”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:
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-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 33Um 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.
Passo 3: Parâmetros de rota
Seção intitulada “Passo 3: Parâmetros de rota”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.
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3Olá, Ana!2 + 3 = 5Agora, 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:
curl -i http://127.0.0.1:3000/add/2/xHTTP/1.1 400 Bad Requestcontent-type: text/plain; charset=utf-8content-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.
Passo 4: Receber JSON com POST
Seção intitulada “Passo 4: Receber JSON com POST”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:
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:
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 Entitycontent-type: text/plain; charset=utf-8content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 24A 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 /todosasync 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 /todosasync 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)mandaGET /todosePOST /todospara handlers diferentes. - Códigos de status são valores de retorno.
StatusCodevem do módulohttpdo axum. Um handler pode retornar umResult:Ok(Json(todo))vira 200 com JSON, eErr(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 umVecnormal. 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
retainmanté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 })Noneapagou a 1? trueapagou a 1 de novo? falserestam: 1A tarefa criada por meio do second_handle aparece quando você lê por meio do db, porque os dois Arcs apontam para a mesma lista.
Passo 6: Teste com o curl
Seção intitulada “Passo 6: Teste com o curl”Inicie o servidor com cargo run e, num segundo terminal, teste cada endpoint. No começo, a lista está vazia:
curl http://127.0.0.1:3000/todos[]Crie uma tarefa. Repare no status 201 Created:
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Comprar leite"}'HTTP/1.1 201 Createdcontent-type: application/jsoncontent-length: 45
{"id":1,"title":"Comprar leite","done":false}Crie uma segunda e liste todas:
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:
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"Aprender axum","done":false}HTTP/1.1 404 Not Foundcontent-length: 0Apague a tarefa 1 e depois tente apagá-la de novo:
curl -i -X DELETE http://127.0.0.1:3000/todos/1curl -i -X DELETE http://127.0.0.1:3000/todos/1HTTP/1.1 204 No Content
HTTP/1.1 404 Not Foundcontent-length: 0curl 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:
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/2HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12HTTP/1.1 405 Method Not Allowedallow: GET,HEAD,DELETEcontent-length: 0A 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.
Erros comuns
Seção intitulada “Erros comuns”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 messageQuando 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.
Ideias para ir além
Seção intitulada “Ideias para ir além”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 comiter_mut(), definadone = truee 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 devolverErr((StatusCode::BAD_REQUEST, String::from("o título não pode ficar vazio"))). Usetitle.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>enext_id: u32dentro doMutex. - Persistência com um arquivo JSON. Derive
Deserializetambém emTodo. Na inicialização, leiatodos.jsoncomstd::fs::read_to_stringeserde_json::from_str(ou comece vazio se o arquivo não existir). Depois de cada mudança, grave a lista de volta comserde_json::to_string_prettyestd::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 traceecargo 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.1só aceita requisições do seu próprio computador. Para aceitar requisições de outras máquinas (por exemplo, dentro do Docker ou num servidor), use0.0.0.0nobind. Muitos serviços de hospedagem dizem ao seu programa qual porta usar por meio de uma variável de ambientePORT, que você pode ler comstd::env::var(veja Ambiente e processos). Compile comcargo build --releasepara ter um servidor bem mais rápido. O resultado é um único arquivo emtarget/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. Oaxum::servefaz tudo rodar numTcpListenerdotokio. - Os extractors tiram dados da requisição:
Pathpara partes da URL (/todos/{id}),Jsonpara o corpo (sempre por último) eStatepara dados compartilhados. - Retorne
Json(valor)para JSON,StatusCodepara um status, uma tupla para definir os dois, ou umResultpara 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:-imostra o status,-Xdefine o método,-Hadiciona um cabeçalho e-denvia um corpo.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Documentação do axum — docs.rs (em inglês): o guia oficial, com seções sobre rotas, handlers e respostas.
- Extractors do axum — docs.rs (em inglês): todos os extractors prontos e as regras para a ordem deles.
- Tutorial do Tokio (em inglês): aprenda o runtime assíncrono por trás do axum, incluindo estado compartilhado.
- Documentação do tower-http — docs.rs (em inglês): camadas prontas para logging, CORS, compressão e timeouts.
- Códigos de status de respostas HTTP — MDN: o que significam 200, 201, 204, 404, 422 e os outros.