Servidor web com axum (introdução)
Nesta aula, você aprenderá a criar um servidor web básico com o framework axum em Rust, explorando os conceitos de rotas, handlers, extractors e respostas. O conteúdo inclui exemplos práticos, boas práticas e exercícios para fixar o aprendizado.
Axum é um framework web moderno e ergonômico para Rust, construído sobre o ecossistema tokio e tower. Ele combina a segurança e performance de Rust com uma API expressiva, inspirada em frameworks como Express (Node.js) e Flask (Python). Nesta aula, vamos introduzir os conceitos fundamentais para construir um servidor web com axum: rotas, handlers, extractors e respostas. Ao final, você terá uma base sólida para criar aplicações web reais, desde APIs REST até páginas dinâmicas.
Axum se destaca por sua composabilidade: cada parte do servidor (como middlewares, rotas e extractors) é um componente reutilizável que pode ser combinado de forma flexível. Além disso, ele é totalmente assíncrono, aproveitando o modelo de concorrência do tokio, o que o torna ideal para aplicações de alta performance. Vamos começar configurando um projeto e construindo nosso primeiro servidor.
Rotas
Rotas são os caminhos que definem como o servidor responde às requisições HTTP. Em axum, uma rota associa um método HTTP (GET, POST, PUT, DELETE, etc.) e um padrão de path a um handler. O mecanismo de roteamento é baseado no crate `matchit`, que oferece alta performance e suporte a parâmetros dinâmicos.
Para definir rotas, usamos o método `route` do objeto `Router`. Cada rota recebe o path (como string) e um handler. O path pode conter segmentos fixos e segmentos dinâmicos, que são capturados e passados ao handler. Por exemplo, `/users/:id` captura o valor do segmento após `/users/` e o disponibiliza como um extractor.
Exemplo básico de rotas:
use axum::{Router, routing::get};
async fn root() -> &'static str {
"Olá, mundo!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Servidor rodando em http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}Neste exemplo, a rota raiz (`/`) responde a requisições GET com o texto "Olá, mundo!". O método `get` é um atalho para `MethodRouter::get`, mas podemos usar `any` para aceitar qualquer método, ou combinar métodos como `get(...).post(...)`.
Rotas com parâmetros dinâmicos são igualmente simples. Vamos criar uma rota que recebe um ID numérico:
use axum::{Router, routing::get, extract::Path};
async fn show_user(Path(id): Path<u32>) -> String {
format!("Usuário {}", id)
}
let app = Router::new()
.route("/users/:id", get(show_user));Note que o handler recebe um `Path
Handlers
Handlers são funções assíncronas que processam uma requisição e retornam uma resposta. Em axum, um handler pode ser uma função assíncrona que recebe zero ou mais extractors como argumentos e retorna algo que implemente `IntoResponse`. Isso inclui tipos como `String`, `&'static str`, `Html`, `Json`, `StatusCode`, entre outros.
A assinatura de um handler é flexível: você pode combinar extractors de várias formas, e axum se encarrega de extrair os dados da requisição e chamar a função com os argumentos corretos. Isso elimina muito código boilerplate e torna os handlers limpos e declarativos.
Vejamos um handler que retorna HTML e outro que retorna JSON:
use axum::{response::Html, Json};
use serde_json::json;
async fn page() -> Html<&'static str> {
Html("<h1>Página inicial</h1>")
}
async fn api_info() -> Json<serde_json::Value> {
Json(json!({
"name": "axum",
"version": "0.7"
}))
}Note que o handler `api_info` retorna `Json
Handlers podem ser combinados em um mesmo path com diferentes métodos. Por exemplo, para uma rota que aceita GET e POST, podemos encadear:
use axum::{routing::{get, post}, Router};
async fn list_items() -> &'static str {
"Lista de itens"
}
async fn create_item() -> &'static str {
"Item criado"
}
let app = Router::new()
.route("/items", get(list_items).post(create_item));Se um método não for suportado, axum retorna automaticamente um 405 Method Not Allowed.
Extractors
Extractors são tipos que implementam a trait `FromRequest` (ou `FromRequestParts` para partes da requisição). Eles permitem extrair informações da requisição de forma tipada e segura. Axum já fornece vários extractors prontos, como `Path`, `Query`, `Json`, `State`, `HeaderMap`, `UserAgent`, entre outros.
O extractor `Path` captura parâmetros da rota. O extractor `Query` extrai parâmetros da query string. O extractor `Json` desserializa o corpo da requisição para um tipo `T` que implemente `serde::Deserialize`. O extractor `State` permite acessar estado compartilhado da aplicação, como uma conexão com banco de dados.
Exemplo com `Query` e `Json`:
use axum::{extract::Query, Json};
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchParams {
q: String,
page: Option<u32>,
}
async fn search(Query(params): Query<SearchParams>) -> String {
format!("Buscando por '{}' na página {:?}", params.q, params.page)
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> String {
format!("Criando usuário {} com email {}", payload.name, payload.email)
}É importante notar que a ordem dos extractors importa: extractors que consomem o corpo da requisição, como `Json`, devem ser os últimos argumentos do handler, pois consomem o corpo. Extratores como `Path` e `Query` podem ser usados em qualquer posição.
Para usar `State`, primeiro precisamos adicionar o estado ao router com `.with_state(state)`. O tipo do estado deve ser `Clone + Send + Sync + 'static`. Exemplo:
use axum::{extract::State, Router};
use std::sync::Arc;
struct AppState {
db: Arc<dyn Database>, // suponha um trait Database
}
async fn get_user(State(state): State<Arc<AppState>>, Path(id): Path<u32>) -> String {
// use state.db para buscar o usuário
format!("Buscando usuário {}", id)
}
let state = Arc::new(AppState { db: /* ... */ });
let app = Router::new()
.route("/users/:id", get(get_user))
.with_state(state);Respostas
Em axum, qualquer tipo que implemente `IntoResponse` pode ser retornado de um handler. Isso inclui tipos primitivos como `String`, `&'static str`, `StatusCode`, `Vec
O tipo `Response` do `http` crate é o tipo base. Você pode construí-lo manualmente usando `Response::builder()`, mas normalmente usamos os atalhos fornecidos.
Exemplo de resposta com status customizado:
use axum::{response::IntoResponse, http::StatusCode};
async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Recurso não encontrado")
}Aqui retornamos uma tupla `(StatusCode, &'static str)`, que também implementa `IntoResponse`. Isso é útil para respostas com corpo e status.
Para retornar JSON, usamos `Json
Exemplo de resposta com `Redirect`:
use axum::response::Redirect;
async fn old_page() -> Redirect {
Redirect::to("/new-page")
}Além disso, você pode implementar `IntoResponse` para seus próprios tipos, permitindo que handlers retornem tipos personalizados de forma limpa. Isso é especialmente útil em aplicações maiores para centralizar a lógica de resposta.
Boas práticas
Ao trabalhar com axum, algumas práticas recomendadas ajudam a manter o código organizado e escalável:
- Use módulos para separar rotas, handlers e estado da aplicação.
- Aproveite os extractors para evitar manipulação manual de requisições.
- Prefira retornar tipos de resposta específicos (como `Json`, `Html`) em vez de `String` quando o conteúdo tiver um formato definido.
- Utilize `State` para compartilhar recursos como conexões de banco de dados, evitando criar novas conexões a cada requisição.
- Trate erros de forma consistente: defina um tipo de erro que implemente `IntoResponse` para retornar respostas de erro padronizadas.
- Use o `Router` para agrupar rotas relacionadas, como `/users` e `/posts`, e depois combine-os com `nest`.
Referências
- Documentação oficial do axum (docs.rs)
- Repositório GitHub do axum
- Documentação do tokio
- Documentação do tower
- Documentação do serde_json
- MDN - HTTP
- Documentação do crate http
Exercícios
Crie um servidor axum com uma rota `/` que retorne a string "Bem-vindo ao axum!".
✓ Resposta:use axum::{Router, routing::get}; async fn root() -> &'static str { "Bem-vindo ao axum!" } #[tokio::main] async fn main() { let app = Router::new().route("/", get(root)); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }Adicione uma rota `/users/:id` que retorne o ID como uma string, por exemplo, "User ID: 42".
✓ Resposta:use axum::{Router, routing::get, extract::Path}; async fn user(Path(id): Path<u32>) -> String { format!("User ID: {}", id) } let app = Router::new().route("/users/:id", get(user));Crie um handler que receba um parâmetro de query `q` e retorne "Searching for: {q}". Use o extractor `Query`.
✓ Resposta:use axum::{extract::Query, Router, routing::get}; use serde::Deserialize; #[derive(Deserialize)] struct SearchParams { q: String, } async fn search(Query(params): Query<SearchParams>) -> String { format!("Searching for: {}", params.q) } let app = Router::new().route("/search", get(search));Implemente uma rota POST `/users` que receba um JSON com `name` e `email` e retorne uma mensagem de sucesso contendo esses dados.
✓ Resposta:use axum::{routing::post, Json, Router}; use serde::Deserialize; #[derive(Deserialize)] struct CreateUser { name: String, email: String, } async fn create_user(Json(payload): Json<CreateUser>) -> String { format!("User {} created with email {}", payload.name, payload.email) } let app = Router::new().route("/users", post(create_user));Use o extractor `State` para compartilhar um valor inteiro (por exemplo, um contador) entre handlers. Crie duas rotas: uma que retorna o valor atual e outra que incrementa e retorna o novo valor (simule com um `Mutex`).
✓ Resposta:use axum::{extract::State, routing::get, Router}; use std::sync::{Arc, Mutex}; struct AppState { counter: Mutex<u32>, } async fn get_counter(State(state): State<Arc<AppState>>) -> String { let counter = state.counter.lock().unwrap(); format!("Counter: {}", *counter) } async fn increment(State(state): State<Arc<AppState>>) -> String { let mut counter = state.counter.lock().unwrap(); *counter += 1; format!("Counter incremented to: {}", *counter) } let state = Arc::new(AppState { counter: Mutex::new(0) }); let app = Router::new() .route("/counter", get(get_counter)) .route("/increment", get(increment)) .with_state(state);