Axum Rust: Framework Web Moderno Guia | Rust Brasil

Guia completo do Axum em Rust: routers, extractors, middleware Tower e APIs REST. O framework web do ecossistema Tokio.

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ísticaAxumActix WebRocketWarp
EcossistemaTokio/TowerPróprioPróprioTokio
PerformanceExcelenteExcelenteBoaExcelente
ErgonomiaMuito boaBoaExcelenteModerada
MacrosMínimasModeradasMuitasNenhuma
MiddlewareTower (vasto)PróprioFairingsFilters
Type safetyForteForteMuito forteMuito forte
WebSocketIntegradoIntegradoLimitadoIntegrado
PopularidadeCrescendo rápidoEstabelecidoEstabelecidoNicho
ManutençãoEquipe TokioComunidadeComunidadeComunidade
Curva de aprendizadoModeradaModeradaBaixaAlta

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 IntoResponse permite 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.