O Axum é um framework web ergonômico e altamente performático, desenvolvido pela equipe do Tokio. Ele se destaca por sua integração profunda com o ecossistema Tower (middleware) e Tokio (runtime assíncrono), oferecendo uma API type-safe onde erros de roteamento, extração de dados e composição de middleware são detectados em tempo de compilação.
Diferente de outros frameworks que implementam suas próprias abstrações de middleware e handlers, o Axum utiliza o trait Service do Tower como base, permitindo reutilizar centenas de middlewares existentes no ecossistema. Ele também elimina macros de roteamento, usando composição de funções puras e tipos genéricos para uma experiência mais transparente e depurável.
O Axum se tornou rapidamente o framework web mais popular do ecossistema Rust, sendo escolhido por empresas como Cloudflare, Shopify e muitas startups para construir APIs de alta performance. Se você está começando um novo projeto web em Rust, o Axum é a escolha recomendada.
Instalação
Adicione o Axum e dependências ao Cargo.toml:
[dependencies]
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.5", features = [
"cors",
"trace",
"compression-gzip",
"timeout",
"limit",
] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Opcionais úteis
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "1.0"
Uso Básico
Hello World
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
// Criar o router
let app = Router::new()
.route("/", get(raiz))
.route("/ola/{nome}", get(saudacao));
// Iniciar o servidor
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Servidor rodando em http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
// Handler simples
async fn raiz() -> &'static str {
"Olá, mundo!"
}
// Handler com parâmetro de rota
async fn saudacao(axum::extract::Path(nome): axum::extract::Path<String>) -> String {
format!("Olá, {}!", nome)
}
Routers e Métodos HTTP
use axum::{
routing::{get, post, put, delete, patch},
Router,
};
fn criar_app() -> Router {
Router::new()
// Rotas básicas
.route("/", get(pagina_inicial))
.route("/sobre", get(sobre))
// Múltiplos métodos na mesma rota
.route("/usuarios", get(listar_usuarios).post(criar_usuario))
.route("/usuarios/{id}", get(obter_usuario).put(atualizar_usuario).delete(deletar_usuario))
// Aninhar routers
.nest("/api/v1", router_api_v1())
.nest("/admin", router_admin())
// Rota fallback (404)
.fallback(pagina_nao_encontrada)
}
fn router_api_v1() -> Router {
Router::new()
.route("/produtos", get(listar_produtos).post(criar_produto))
.route("/produtos/{id}", get(obter_produto))
.route("/pedidos", get(listar_pedidos).post(criar_pedido))
}
fn router_admin() -> Router {
Router::new()
.route("/dashboard", get(dashboard))
.route("/usuarios", get(admin_listar_usuarios))
}
// Handlers placeholder
async fn pagina_inicial() -> &'static str { "Página inicial" }
async fn sobre() -> &'static str { "Sobre" }
async fn listar_usuarios() -> &'static str { "Lista de usuários" }
async fn criar_usuario() -> &'static str { "Usuário criado" }
async fn obter_usuario() -> &'static str { "Detalhes do usuário" }
async fn atualizar_usuario() -> &'static str { "Usuário atualizado" }
async fn deletar_usuario() -> &'static str { "Usuário deletado" }
async fn listar_produtos() -> &'static str { "Lista de produtos" }
async fn criar_produto() -> &'static str { "Produto criado" }
async fn obter_produto() -> &'static str { "Detalhes do produto" }
async fn listar_pedidos() -> &'static str { "Lista de pedidos" }
async fn criar_pedido() -> &'static str { "Pedido criado" }
async fn dashboard() -> &'static str { "Dashboard admin" }
async fn admin_listar_usuarios() -> &'static str { "Admin: usuários" }
async fn pagina_nao_encontrada() -> (axum::http::StatusCode, &'static str) {
(axum::http::StatusCode::NOT_FOUND, "Página não encontrada")
}
Extractors - Extraindo Dados de Requisições
use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
response::IntoResponse,
Router,
};
use serde::{Deserialize, Serialize};
// === Path: parâmetros da URL ===
async fn obter_usuario(Path(id): Path<u64>) -> String {
format!("Usuário ID: {}", id)
}
// Múltiplos parâmetros
async fn obter_post_de_usuario(
Path((user_id, post_id)): Path<(u64, u64)>,
) -> String {
format!("Usuário {} - Post {}", user_id, post_id)
}
// === Query: parâmetros de query string ===
#[derive(Deserialize)]
struct Paginacao {
#[serde(default = "pagina_padrao")]
pagina: u32,
#[serde(default = "limite_padrao")]
limite: u32,
busca: Option<String>,
}
fn pagina_padrao() -> u32 { 1 }
fn limite_padrao() -> u32 { 20 }
// GET /usuarios?pagina=2&limite=10&busca=maria
async fn listar_usuarios(Query(params): Query<Paginacao>) -> String {
format!(
"Página {}, limite {}, busca: {:?}",
params.pagina, params.limite, params.busca
)
}
// === Json: corpo da requisição ===
#[derive(Deserialize)]
struct CriarUsuarioRequest {
nome: String,
email: String,
#[serde(default)]
ativo: bool,
}
#[derive(Serialize)]
struct UsuarioResponse {
id: u64,
nome: String,
email: String,
ativo: bool,
}
async fn criar_usuario(
Json(payload): Json<CriarUsuarioRequest>,
) -> (StatusCode, Json<UsuarioResponse>) {
let usuario = UsuarioResponse {
id: 1,
nome: payload.nome,
email: payload.email,
ativo: payload.ativo,
};
(StatusCode::CREATED, Json(usuario))
}
// === Headers ===
use axum::http::HeaderMap;
async fn ver_headers(headers: HeaderMap) -> String {
let user_agent = headers
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("desconhecido");
format!("User-Agent: {}", user_agent)
}
// === Múltiplos extractors combinados ===
async fn atualizar_usuario(
Path(id): Path<u64>,
State(estado): State<AppState>,
Json(payload): Json<CriarUsuarioRequest>,
) -> impl IntoResponse {
// Acessar Path, State e Json na mesma handler
let resposta = format!(
"Atualizando usuário {} com nome '{}' (app: {})",
id, payload.nome, estado.nome_app
);
(StatusCode::OK, resposta)
}
#[derive(Clone)]
struct AppState {
nome_app: String,
}
Recursos Avançados
State Management
use axum::{extract::State, routing::get, Router};
use std::sync::Arc;
use tokio::sync::RwLock;
// Estado compartilhado da aplicação
#[derive(Clone)]
struct AppState {
// Dados imutáveis
nome_app: String,
versao: String,
// Dados mutáveis (protegidos por RwLock)
contadores: Arc<RwLock<Contadores>>,
}
struct Contadores {
requisicoes: u64,
erros: u64,
}
async fn status(State(state): State<AppState>) -> String {
let contadores = state.contadores.read().await;
format!(
"{} v{} - Requisições: {}, Erros: {}",
state.nome_app, state.versao,
contadores.requisicoes, contadores.erros
)
}
async fn incrementar_requisicoes(State(state): State<AppState>) -> &'static str {
let mut contadores = state.contadores.write().await;
contadores.requisicoes += 1;
"OK"
}
#[tokio::main]
async fn main() {
let state = AppState {
nome_app: "MeuApp".to_string(),
versao: "1.0.0".to_string(),
contadores: Arc::new(RwLock::new(Contadores {
requisicoes: 0,
erros: 0,
})),
};
let app = Router::new()
.route("/status", get(status))
.route("/incrementar", get(incrementar_requisicoes))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Middleware com Tower
use axum::{
middleware::{self, Next},
extract::Request,
response::Response,
Router,
routing::get,
http::StatusCode,
};
use tower_http::{
cors::{CorsLayer, Any},
trace::TraceLayer,
compression::CompressionLayer,
timeout::TimeoutLayer,
limit::RequestBodyLimitLayer,
};
use std::time::{Duration, Instant};
// Middleware customizado: logging de tempo de resposta
async fn logging_middleware(
req: Request,
next: Next,
) -> Response {
let metodo = req.method().clone();
let uri = req.uri().clone();
let inicio = Instant::now();
let response = next.run(req).await;
let tempo = inicio.elapsed();
let status = response.status();
tracing::info!(
"{} {} -> {} ({:?})",
metodo, uri, status, tempo
);
response
}
// Middleware customizado: autenticação
async fn auth_middleware(
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
// Token válido, continuar
Ok(next.run(req).await)
}
_ => {
// Sem token ou token inválido
Err(StatusCode::UNAUTHORIZED)
}
}
}
fn criar_app() -> Router {
// Rotas públicas (sem autenticação)
let rotas_publicas = Router::new()
.route("/", get(|| async { "Bem-vindo!" }))
.route("/saude", get(|| async { "OK" }));
// Rotas protegidas (com autenticação)
let rotas_protegidas = Router::new()
.route("/perfil", get(|| async { "Seu perfil" }))
.route("/dados", get(|| async { "Dados privados" }))
.layer(middleware::from_fn(auth_middleware));
// CORS
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
// Combinar e aplicar middleware global
Router::new()
.merge(rotas_publicas)
.merge(rotas_protegidas)
.layer(middleware::from_fn(logging_middleware))
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1MB
.layer(cors)
}
Tratamento de Erros
use axum::{
extract::Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
use thiserror::Error;
// Enum de erros da aplicação
#[derive(Error, Debug)]
enum AppError {
#[error("Recurso não encontrado: {0}")]
NaoEncontrado(String),
#[error("Dados inválidos: {0}")]
Validacao(String),
#[error("Não autorizado")]
NaoAutorizado,
#[error("Erro interno: {0}")]
Interno(#[from] anyhow::Error),
}
// Resposta de erro serializada
#[derive(Serialize)]
struct ErroResponse {
codigo: u16,
mensagem: String,
#[serde(skip_serializing_if = "Option::is_none")]
detalhes: Option<String>,
}
// Converter AppError em Response HTTP
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, mensagem, detalhes) = match &self {
AppError::NaoEncontrado(recurso) => (
StatusCode::NOT_FOUND,
"Recurso não encontrado".to_string(),
Some(recurso.clone()),
),
AppError::Validacao(msg) => (
StatusCode::BAD_REQUEST,
"Dados inválidos".to_string(),
Some(msg.clone()),
),
AppError::NaoAutorizado => (
StatusCode::UNAUTHORIZED,
"Não autorizado".to_string(),
None,
),
AppError::Interno(err) => {
tracing::error!("Erro interno: {:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Erro interno do servidor".to_string(),
None, // Não expor detalhes internos
)
}
};
let body = ErroResponse {
codigo: status.as_u16(),
mensagem,
detalhes,
};
(status, Json(body)).into_response()
}
}
// Usar em handlers
async fn obter_usuario(
axum::extract::Path(id): axum::extract::Path<u64>,
) -> Result<Json<serde_json::Value>, AppError> {
if id == 0 {
return Err(AppError::Validacao("ID deve ser maior que zero".to_string()));
}
if id > 1000 {
return Err(AppError::NaoEncontrado(format!("Usuário {}", id)));
}
Ok(Json(serde_json::json!({
"id": id,
"nome": "Maria Silva"
})))
}
WebSocket
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::IntoResponse,
routing::get,
Router,
};
use futures::{SinkExt, StreamExt};
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(tratar_websocket)
}
async fn tratar_websocket(mut socket: WebSocket) {
// Enviar mensagem de boas-vindas
if socket
.send(Message::Text("Conectado ao WebSocket!".to_string()))
.await
.is_err()
{
return;
}
// Loop de mensagens
while let Some(msg) = socket.recv().await {
match msg {
Ok(Message::Text(texto)) => {
println!("Recebido: {}", texto);
// Echo
let resposta = format!("Você disse: {}", texto);
if socket.send(Message::Text(resposta)).await.is_err() {
break;
}
}
Ok(Message::Ping(dados)) => {
if socket.send(Message::Pong(dados)).await.is_err() {
break;
}
}
Ok(Message::Close(_)) => {
println!("Cliente desconectou");
break;
}
Err(e) => {
eprintln!("Erro no WebSocket: {}", e);
break;
}
_ => {}
}
}
}
fn criar_app_ws() -> Router {
Router::new()
.route("/ws", get(ws_handler))
}
Boas Práticas
1. Organize Rotas em Módulos
// src/routes/mod.rs
mod usuarios;
mod produtos;
mod auth;
use axum::Router;
use crate::AppState;
pub fn criar_router() -> Router<AppState> {
Router::new()
.merge(auth::router())
.nest("/api/v1", api_v1_router())
}
fn api_v1_router() -> Router<AppState> {
Router::new()
.nest("/usuarios", usuarios::router())
.nest("/produtos", produtos::router())
}
// src/routes/usuarios.rs
use axum::{routing::{get, post}, Router, extract::{State, Path, Json}};
use crate::{AppState, models::Usuario};
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(listar).post(criar))
.route("/{id}", get(obter).put(atualizar).delete(deletar))
}
async fn listar(State(state): State<AppState>) -> Json<Vec<Usuario>> {
// implementação
Json(vec![])
}
async fn criar(
State(state): State<AppState>,
Json(payload): Json<Usuario>,
) -> Json<Usuario> {
Json(payload)
}
async fn obter(Path(id): Path<u64>) -> Json<Usuario> {
todo!()
}
async fn atualizar(Path(id): Path<u64>, Json(payload): Json<Usuario>) -> Json<Usuario> {
todo!()
}
async fn deletar(Path(id): Path<u64>) -> axum::http::StatusCode {
axum::http::StatusCode::NO_CONTENT
}
2. Use Extractors Customizados
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, StatusCode},
};
// Extractor customizado para autenticação
struct UsuarioAutenticado {
id: u64,
nome: String,
permissoes: Vec<String>,
}
#[async_trait]
impl<S> FromRequestParts<S> for UsuarioAutenticado
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let token = parts
.headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or((StatusCode::UNAUTHORIZED, "Token ausente".to_string()))?;
// Validar token (simplificado)
if token == "token-valido" {
Ok(UsuarioAutenticado {
id: 1,
nome: "Maria".to_string(),
permissoes: vec!["admin".to_string()],
})
} else {
Err((StatusCode::UNAUTHORIZED, "Token inválido".to_string()))
}
}
}
// Usar como extractor no handler
async fn perfil(usuario: UsuarioAutenticado) -> String {
format!("Olá, {}! Permissões: {:?}", usuario.nome, usuario.permissoes)
}
3. Configure Graceful Shutdown
use axum::Router;
use tokio::signal;
#[tokio::main]
async fn main() {
let app = Router::new(); // suas rotas aqui
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Servidor rodando em http://localhost:3000");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
println!("Servidor encerrado gracefully");
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Falha ao instalar handler Ctrl+C");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Falha ao instalar handler SIGTERM")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("Sinal de shutdown recebido...");
}
4. Logging Estruturado
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn inicializar_logging() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "meu_app=debug,tower_http=debug,axum=trace".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
}
Exemplos Práticos
Exemplo: API REST Completa com CRUD
use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post, put, delete},
Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use chrono::{DateTime, Utc};
// === Modelos ===
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Produto {
id: String,
nome: String,
descricao: String,
preco: f64,
categoria: String,
estoque: u32,
ativo: bool,
criado_em: DateTime<Utc>,
atualizado_em: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
struct CriarProdutoRequest {
nome: String,
descricao: String,
preco: f64,
categoria: String,
#[serde(default)]
estoque: u32,
}
#[derive(Debug, Deserialize)]
struct AtualizarProdutoRequest {
nome: Option<String>,
descricao: Option<String>,
preco: Option<f64>,
categoria: Option<String>,
estoque: Option<u32>,
ativo: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct FiltrosProduto {
#[serde(default = "pagina_padrao")]
pagina: u32,
#[serde(default = "limite_padrao")]
limite: u32,
categoria: Option<String>,
busca: Option<String>,
#[serde(default)]
apenas_ativos: bool,
}
fn pagina_padrao() -> u32 { 1 }
fn limite_padrao() -> u32 { 20 }
#[derive(Serialize)]
struct ListaPaginada<T> {
dados: Vec<T>,
pagina: u32,
limite: u32,
total: usize,
total_paginas: u32,
}
#[derive(Serialize)]
struct MensagemResponse {
mensagem: String,
}
// === Estado da Aplicação ===
type Db = Arc<RwLock<HashMap<String, Produto>>>;
#[derive(Clone)]
struct AppState {
db: Db,
}
// === Handlers ===
// POST /produtos
async fn criar_produto(
State(state): State<AppState>,
Json(payload): Json<CriarProdutoRequest>,
) -> Result<(StatusCode, Json<Produto>), (StatusCode, Json<MensagemResponse>)> {
// Validação
if payload.nome.trim().is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(MensagemResponse { mensagem: "Nome é obrigatório".to_string() }),
));
}
if payload.preco < 0.0 {
return Err((
StatusCode::BAD_REQUEST,
Json(MensagemResponse { mensagem: "Preço deve ser positivo".to_string() }),
));
}
let agora = Utc::now();
let produto = Produto {
id: Uuid::new_v4().to_string(),
nome: payload.nome,
descricao: payload.descricao,
preco: payload.preco,
categoria: payload.categoria,
estoque: payload.estoque,
ativo: true,
criado_em: agora,
atualizado_em: agora,
};
let mut db = state.db.write().await;
db.insert(produto.id.clone(), produto.clone());
Ok((StatusCode::CREATED, Json(produto)))
}
// GET /produtos
async fn listar_produtos(
State(state): State<AppState>,
Query(filtros): Query<FiltrosProduto>,
) -> Json<ListaPaginada<Produto>> {
let db = state.db.read().await;
let mut produtos: Vec<Produto> = db.values()
.filter(|p| {
// Filtro de ativos
if filtros.apenas_ativos && !p.ativo {
return false;
}
// Filtro de categoria
if let Some(ref cat) = filtros.categoria {
if &p.categoria != cat {
return false;
}
}
// Filtro de busca
if let Some(ref busca) = filtros.busca {
let busca = busca.to_lowercase();
if !p.nome.to_lowercase().contains(&busca)
&& !p.descricao.to_lowercase().contains(&busca)
{
return false;
}
}
true
})
.cloned()
.collect();
// Ordenar por data de criação (mais recente primeiro)
produtos.sort_by(|a, b| b.criado_em.cmp(&a.criado_em));
let total = produtos.len();
let total_paginas = ((total as f64) / (filtros.limite as f64)).ceil() as u32;
// Paginação
let inicio = ((filtros.pagina - 1) * filtros.limite) as usize;
let fim = (inicio + filtros.limite as usize).min(total);
let dados = if inicio < total {
produtos[inicio..fim].to_vec()
} else {
vec![]
};
Json(ListaPaginada {
dados,
pagina: filtros.pagina,
limite: filtros.limite,
total,
total_paginas,
})
}
// GET /produtos/:id
async fn obter_produto(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<Produto>, (StatusCode, Json<MensagemResponse>)> {
let db = state.db.read().await;
db.get(&id)
.cloned()
.map(Json)
.ok_or((
StatusCode::NOT_FOUND,
Json(MensagemResponse {
mensagem: format!("Produto '{}' não encontrado", id),
}),
))
}
// PUT /produtos/:id
async fn atualizar_produto(
State(state): State<AppState>,
Path(id): Path<String>,
Json(payload): Json<AtualizarProdutoRequest>,
) -> Result<Json<Produto>, (StatusCode, Json<MensagemResponse>)> {
let mut db = state.db.write().await;
let produto = db.get_mut(&id).ok_or((
StatusCode::NOT_FOUND,
Json(MensagemResponse {
mensagem: format!("Produto '{}' não encontrado", id),
}),
))?;
// Atualizar apenas campos fornecidos
if let Some(nome) = payload.nome {
produto.nome = nome;
}
if let Some(descricao) = payload.descricao {
produto.descricao = descricao;
}
if let Some(preco) = payload.preco {
produto.preco = preco;
}
if let Some(categoria) = payload.categoria {
produto.categoria = categoria;
}
if let Some(estoque) = payload.estoque {
produto.estoque = estoque;
}
if let Some(ativo) = payload.ativo {
produto.ativo = ativo;
}
produto.atualizado_em = Utc::now();
Ok(Json(produto.clone()))
}
// DELETE /produtos/:id
async fn deletar_produto(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<MensagemResponse>)> {
let mut db = state.db.write().await;
if db.remove(&id).is_some() {
Ok(StatusCode::NO_CONTENT)
} else {
Err((
StatusCode::NOT_FOUND,
Json(MensagemResponse {
mensagem: format!("Produto '{}' não encontrado", id),
}),
))
}
}
// === Aplicação ===
#[tokio::main]
async fn main() {
// Inicializar logging
tracing_subscriber::fmt()
.with_target(false)
.compact()
.init();
let state = AppState {
db: Arc::new(RwLock::new(HashMap::new())),
};
let app = Router::new()
.route("/produtos", get(listar_produtos).post(criar_produto))
.route("/produtos/{id}", get(obter_produto).put(atualizar_produto).delete(deletar_produto))
.route("/saude", get(|| async { Json(serde_json::json!({"status": "ok"})) }))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("API rodando em http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Comparação com Alternativas
| Característica | Axum | Actix Web | Rocket | Warp |
|---|---|---|---|---|
| Ecossistema | Tokio/Tower | Próprio | Próprio | Tokio |
| Performance | Excelente | Excelente | Boa | Excelente |
| Ergonomia | Muito boa | Boa | Excelente | Moderada |
| Macros | Mínimas | Moderadas | Muitas | Nenhuma |
| Middleware | Tower (vasto) | Próprio | Fairings | Filters |
| Type safety | Forte | Forte | Muito forte | Muito forte |
| WebSocket | Integrado | Integrado | Limitado | Integrado |
| Popularidade | Crescendo rápido | Estabelecido | Estabelecido | Nicho |
| Manutenção | Equipe Tokio | Comunidade | Comunidade | Comunidade |
| Curva de aprendizado | Moderada | Moderada | Baixa | Alta |
O Axum se destaca por:
- Integração com Tower: acesso a centenas de middlewares existentes
- Sem macros: roteamento via composição de funções
- Extractors type-safe: erros detectados em tempo de compilação
- Equipe Tokio: manutenção ativa e integração perfeita
- Modularidade: fácil compor e reestruturar aplicações
Conclusão
O Axum representa o estado da arte em frameworks web Rust, combinando ergonomia, type safety e performance excepcional. Sua integração com o ecossistema Tower/Tokio oferece uma base sólida para construir desde APIs simples até sistemas distribuídos complexos.
Pontos-chave para lembrar:
- Router compõe rotas de forma declarativa sem macros
- Extractors extraem dados de requisições com type safety
- State compartilha dados entre handlers via
Arc<RwLock<T>> - Tower middleware oferece logging, CORS, compressão, rate limiting e mais
- Tratamento de erros com
IntoResponsepermite respostas customizadas - Graceful shutdown com sinais do sistema operacional
Para aprofundar, consulte a documentação oficial do Axum e os exemplos no GitHub.
Para uma alternativa com modelo de atores, confira o Actix Web.