Introdução ao Axum e sua Arquitetura
Axum é um framework web moderno construído sobre a stack assíncrona de Rust, particularmente o Tokio. Diferentemente de frameworks tradicionais, Axum trabalha com composição de componentes através de towers e middlewares, permitindo máxima flexibilidade e performance. O framework é mantido pela equipe Tokio e representa o estado da arte em desenvolvimento web com Rust, sendo ideal para APIs REST que demandam alta concorrência e baixa latência.
A arquitetura de Axum segue o padrão de roteadores compostos e handlers tipados. Cada rota é associada a um handler function que recebe extratos da requisição (como JSON bodies ou parâmetros de path) de forma type-safe. Isso significa que erros de tipo são capturados em tempo de compilação, não em runtime como em muitas linguagens dinâmicas.
Configurando seu Primeiro Projeto
Comece criando um novo projeto Rust e adicionando as dependências essenciais no Cargo.toml:
[package]
name = "api-axum"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Agora crie uma API simples com um endpoint que retorna JSON:
use axum::{
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<User>) -> Json<User> {
Json(User {
id: 1,
name: payload.name,
email: payload.email,
})
}
async fn list_users() -> Json<Vec<User>> {
Json(vec![
User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
},
])
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user))
.route("/users", get(list_users));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Execute com cargo run e teste com curl: curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"id":1,"name":"Bob","email":"bob@test.com"}'
Roteamento Avançado e Handlers Tipados
Parâmetros de Path e Query
Axum permite extrair parâmetros diretamente através do sistema de tipos. Para parâmetros de path, use :param na rota:
use axum::extract::Path;
async fn get_user(Path(user_id): Path<u32>) -> Json<User> {
Json(User {
id: user_id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
})
}
let app = Router::new()
.route("/users/:id", get(get_user));
Para query parameters, use Query:
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct ListParams {
limit: Option<u32>,
offset: Option<u32>,
}
async fn list_users_paginated(
Query(params): Query<ListParams>,
) -> Json<Vec<User>> {
let limit = params.limit.unwrap_or(10);
// Implementar lógica de paginação
Json(vec![])
}
let app = Router::new()
.route("/users", get(list_users_paginated));
Estados Compartilhados
Aplicações reais precisam compartilhar estado entre handlers. Use State para injetar dados:
use axum::extract::State;
use std::sync::Arc;
use std::sync::Mutex;
type SharedUsers = Arc<Mutex<Vec<User>>>;
async fn create_user_with_state(
State(users): State<SharedUsers>,
Json(payload): Json<User>,
) -> Json<User> {
let mut users_lock = users.lock().unwrap();
users_lock.push(payload.clone());
Json(payload)
}
#[tokio::main]
async fn main() {
let users_state: SharedUsers = Arc::new(Mutex::new(vec![]));
let app = Router::new()
.route("/users", post(create_user_with_state))
.with_state(users_state);
// ... rest do código
}
Tratamento de Erros e Middlewares
Custom Error Responses
Defina tipos de erro que implementem IntoResponse para respostas HTTP customizadas:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
enum ApiError {
UserNotFound,
InvalidInput,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
ApiError::UserNotFound => (StatusCode::NOT_FOUND, "User not found"),
ApiError::InvalidInput => (StatusCode::BAD_REQUEST, "Invalid input"),
};
(status, Json(serde_json::json!({"error": error_message}))).into_response()
}
}
async fn get_user_safe(
Path(user_id): Path<u32>,
) -> Result<Json<User>, ApiError> {
if user_id == 0 {
return Err(ApiError::InvalidInput);
}
Ok(Json(User {
id: user_id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
}))
}
Middlewares com Tower
Axum integra perfeitamente com Tower para adicionar funcionalidades cross-cutting:
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/users", post(create_user))
.route("/users/:id", get(get_user_safe))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive());
Adicione ao Cargo.toml: tower-http = { version = "0.5", features = ["cors", "trace"] }
Conclusão
Você aprendeu que Axum oferece type-safety e composição elegante para construir APIs REST robustas, permitindo que erros sejam capturados em compilação. O roteamento é intuitivo e as extrações de dados automáticas reduzem boilerplate significativamente. Finalmente, a integração com Tower e o ecosistema Tokio posiciona Axum como escolha ideal para aplicações de alta performance em produção.