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.
Por que Axum é uma boa aposta em 2026
A busca por Axum Rust normalmente vem de três perfis. O primeiro é quem já conhece Rust e quer sair do terminal para construir uma API HTTP real. O segundo é quem trabalha com backend em Go, Node.js, Java ou Python e está avaliando se Rust faz sentido para serviços de baixa latência. O terceiro é quem viu vagas pedindo Tokio, Tower, Hyper, Axum ou Actix Web e precisa entender qual stack estudar primeiro.
Para esses três perfis, Axum é um ótimo ponto de entrada porque fica no centro do ecossistema assíncrono moderno. Ele usa Tokio como runtime, conversa naturalmente com Tower para middleware, aproveita Serde para JSON, integra bem com Tracing para observabilidade e permite evoluir de um CRUD simples para uma API de produção sem trocar de mentalidade.
O principal ganho não é apenas performance. O ganho é previsibilidade. Em uma API Axum bem modelada, handlers deixam claro quais dados extraem da requisição, quais estados compartilham, quais erros podem virar resposta HTTP e quais camadas de middleware atravessam cada rota. Isso reduz a quantidade de comportamento implícito comum em frameworks dinâmicos.
Quando escolher Axum em vez de Actix Web
Se a sua pergunta é Axum vs Actix Web, a resposta curta é: escolha Axum quando você quer integração máxima com Tokio/Tower e composição explícita; escolha Actix Web quando você já tem projeto Actix, depende do ecossistema dele ou quer seguir uma stack mais antiga e consolidada. Os dois são rápidos. A diferença prática aparece em manutenção, middleware e ergonomia de equipe.
Axum tende a ser mais fácil de encaixar em plataformas modernas porque Tower virou uma abstração comum para serviços Rust. Rate limit, timeout, trace, CORS, compressão e camadas customizadas seguem o mesmo modelo mental. Isso é valioso em times que precisam manter gateways, APIs internas, BFFs, microservices e serviços de plataforma com padrões compartilhados.
Para uma comparação detalhada, veja também Axum vs Actix Web em 2026. Use este guia como referência prática de Axum; use a comparação quando a decisão técnica for entre frameworks.
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();
}
Padrão de projeto recomendado para API Axum
Em projetos pequenos, colocar tudo no main.rs ajuda a aprender. Em produção, prefira separar a aplicação em módulos para reduzir acoplamento:
src/
├── main.rs # bootstrap: config, tracing, bind e shutdown
├── app.rs # montagem do Router principal
├── state.rs # AppState e dependências compartilhadas
├── error.rs # tipos de erro + IntoResponse
├── routes/
│ ├── mod.rs
│ ├── health.rs
│ ├── usuarios.rs
│ └── produtos.rs
├── domain/ # regras de negócio puras
├── repositories/ # acesso a Postgres, Redis, filas etc.
└── telemetry.rs # tracing, métricas e correlação
Essa separação evita que Axum vire apenas uma coleção de handlers gigantes. O handler deve traduzir HTTP para domínio: extrair Path, Query, Json e State, chamar uma função de aplicação e converter o resultado em resposta. Regras de negócio, validações complexas e acesso a banco devem ficar fora do handler sempre que possível.
Também vale padronizar desde cedo:
AppStateimutável com pools e clientes clonáveis, comosqlx::PgPool, clients HTTP e configurações.- Um tipo
AppErrorimplementandoIntoResponse, para não espalhar tuplas de erro em todo handler. TraceLayere logs estruturados antes de escrever muitas rotas.- Testes de unidade para domínio e testes HTTP para o
Router. - Timeouts e limites de payload nas bordas, principalmente em endpoints públicos.
Checklist para colocar Axum em produção
Antes de publicar uma API Axum, revise estes pontos:
- Configuração por ambiente: portas, URLs, segredos e flags devem vir de variáveis de ambiente ou arquivos de configuração, não de constantes no código.
- Observabilidade: use Tracing com spans por request, status code, latência, erro e identificador de correlação.
- Graceful shutdown: trate
SIGTERMpara deploys em Kubernetes, systemd, Fly.io, Render, Railway ou servidores próprios. - Banco de dados: se usar Postgres, veja SQLx ou Diesel e mantenha migrations versionadas.
- Erros consistentes: retorne JSON de erro com código interno, mensagem segura e status HTTP correto.
- Segurança básica: CORS explícito, limites de body, headers de segurança no reverse proxy e validação de input.
- Build e deploy: combine
cargo build --releasecom Rust em Docker e cache de dependências. - Teste de carga simples: rode pelo menos um teste com
wrk,k6ou ferramenta equivalente para detectar gargalos óbvios.
Esse checklist também ajuda em entrevistas. Muitas pessoas sabem escrever o hello world; menos pessoas conseguem explicar como uma API Axum sobrevive a deploy, pico de tráfego, erro de banco e investigação em produção.
Axum para carreira Rust no Brasil
Axum aparece bem em portfólios porque demonstra Rust aplicado a produto, não apenas algoritmos. Um projeto de portfólio convincente pode ser uma API REST com autenticação, Postgres, testes, Docker, logs estruturados e documentação OpenAPI. Isso conversa diretamente com vagas Rust no Brasil, especialmente em backend, infraestrutura, fintech, observabilidade e ferramentas internas.
Se você já vem de backend web, Axum é uma ponte natural. Você pode comparar suas decisões com frameworks que conhece: middleware no Express ou NestJS, handlers no FastAPI, controllers no Spring, routers no Go. A diferença é que Rust força a explicitar ownership, erros, concorrência e tipos de dados. Esse atrito inicial vira diferencial quando o serviço precisa ser previsível.
Para estudar de forma prática, siga esta sequência:
- Leia Tokio para entender runtime, tasks e I/O assíncrono.
- Reforce Serde para JSON de entrada e saída.
- Faça o tutorial de API REST com Axum.
- Compare trade-offs em Axum vs Actix Web.
- Publique um projeto com Docker e documente decisões de produção.
- Use a página de empresas que contratam Rust para mapear setores onde esse projeto faz sentido.
Perguntas frequentes sobre Axum Rust
Axum é melhor que Actix Web?
Depende do critério. Axum costuma ser melhor quando você valoriza integração com Tokio e Tower, composição explícita e middleware reutilizável. Actix Web continua forte e maduro. Para projetos novos em 2026, Axum é uma escolha muito segura, especialmente se o time já usa Tokio.
Axum serve para produção?
Sim. Axum é usado em produção para APIs REST, gateways, serviços internos, backends de produto e ferramentas de infraestrutura. O que define prontidão para produção não é só o framework: é configuração, observabilidade, tratamento de erros, deploy, testes e limites operacionais.
Preciso aprender Tokio antes de Axum?
Você consegue começar com exemplos simples sem dominar Tokio, mas para trabalhar bem com Axum precisa entender o básico de async Rust, tasks, .await, timeouts e compartilhamento de estado. O guia de Tokio é o próximo passo natural.
Axum é bom para iniciantes em Rust?
É bom para quem já passou pelos fundamentos da linguagem. Se ownership, borrowing, Result, traits e módulos ainda estão confusos, comece por como aprender Rust em 2026. Depois volte para Axum com um projeto pequeno.
Qual banco usar com Axum?
PostgreSQL com SQLx é uma combinação comum porque oferece async nativo e checagem forte de queries. Diesel também é uma opção madura. Para APIs menores, SQLite com SQLx pode ser suficiente.
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. Para transformar este conhecimento em projeto completo, siga o tutorial de API REST com Axum e depois veja vagas Rust para entender como esse conjunto aparece no mercado.