HTTP server com Axum ou Actix-web
1. Introdução aos frameworks web em Rust
1.1. Por que Axum e Actix-web se destacam no ecossistema Rust
Rust oferece múltiplas opções para construção de servidores HTTP, mas Axum e Actix-web emergiram como os frameworks mais robustos e amplamente adotados. Ambos aproveitam o sistema de tipos do Rust para garantir segurança em tempo de compilação, prevenindo erros comuns como vazamentos de memória e condições de corrida. Axum, construído sobre a biblioteca Tower, integra-se perfeitamente com o ecossistema Tokio e oferece uma abordagem modular e composicional. Actix-web, por sua vez, é conhecido por sua arquitetura orientada a atores e desempenho excepcional em benchmarks.
1.2. Visão geral da arquitetura
Ambos frameworks dependem do Tokio como runtime assíncrono. Axum utiliza o modelo de "serviços" do Tower, onde cada componente é um Service que pode ser combinado e transformado através de layers. Actix-web adota um modelo baseado em atores, onde cada worker processa requisições de forma concorrente, comunicando-se através de mensagens.
1.3. Critérios de escolha
- Performance: Actix-web lidera benchmarks de throughput, mas Axum oferece desempenho competitivo com menor consumo de memória.
- Ergonomia: Axum possui uma API mais declarativa e familiar para quem vem de frameworks como Express.js. Actix-web tem uma curva de aprendizado mais íngreme devido ao modelo de atores.
- Ecossistema: Axum integra-se naturalmente com Tower, permitindo reutilizar middlewares de toda a comunidade Tower. Actix-web possui seu próprio ecossistema de middlewares e plugins.
- Maturidade: Actix-web é mais maduro (versão 4.x), enquanto Axum (versão 0.7) evolui rapidamente com o suporte da equipe Tokio.
2. Configuração inicial e Hello World
2.1. Dependências no Cargo.toml
Para Axum:
[package]
name = "axum-hello"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
Para Actix-web:
[package]
name = "actix-hello"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
actix-cors = "0.7"
2.2. Exemplo mínimo com Axum
use axum::{Router, routing::get, response::Html};
async fn hello_handler() -> Html<&'static str> {
Html("<h1>Hello from Axum!</h1>")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Axum server running on http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
2.3. Exemplo mínimo com Actix-web
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
async fn hello_handler() -> impl Responder {
HttpResponse::Ok().body("<h1>Hello from Actix-web!</h1>")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Actix-web server running on http://127.0.0.1:8080");
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
3. Roteamento e handlers
3.1. Rotas estáticas e dinâmicas
Axum:
use axum::{Router, routing::get, extract::Path, Json};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserQuery {
name: Option<String>,
age: Option<u8>,
}
async fn get_user(Path(id): Path<u32>, query: axum::extract::Query<UserQuery>) -> Json<serde_json::Value> {
Json(serde_json::json!({
"id": id,
"name": query.name.unwrap_or_default(),
"age": query.age
}))
}
let app = Router::new()
.route("/users/:id", get(get_user));
Actix-web:
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserQuery {
name: Option<String>,
age: Option<u8>,
}
async fn get_user(path: web::Path<u32>, query: web::Query<UserQuery>) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().json(serde_json::json!({
"id": id,
"name": query.name.clone().unwrap_or_default(),
"age": query.age
}))
}
let app = App::new()
.route("/users/{id}", web::get().to(get_user));
3.2. Handlers assíncronos e estados compartilhados
Axum com AppState:
use std::sync::Arc;
use axum::{Router, routing::get, extract::State};
struct AppState {
db_pool: Arc<tokio::sync::Mutex<Vec<String>>>,
}
async fn list_items(State(state): State<Arc<AppState>>) -> String {
let db = state.db_pool.lock().await;
format!("Items: {:?}", *db)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
db_pool: Arc::new(tokio::sync::Mutex::new(vec!["item1".to_string()])),
});
let app = Router::new()
.route("/items", get(list_items))
.with_state(state);
}
Actix-web com Data:
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use std::sync::Mutex;
struct AppState {
db_pool: Mutex<Vec<String>>,
}
async fn list_items(data: web::Data<AppState>) -> impl Responder {
let db = data.db_pool.lock().unwrap();
HttpResponse::Ok().body(format!("Items: {:?}", *db))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
db_pool: Mutex::new(vec!["item1".to_string()]),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.route("/items", web::get().to(list_items))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
3.3. Agrupamento de rotas
Axum:
use axum::{Router, routing::get};
let api_routes = Router::new()
.route("/users", get(|| async { "users" }))
.route("/posts", get(|| async { "posts" }));
let app = Router::new()
.nest("/api/v1", api_routes);
Actix-web:
use actix_web::{web, App, HttpServer, Responder};
fn api_scope(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("/users").route(web::get().to(|| async { "users" })));
cfg.service(web::resource("/posts").route(web::get().to(|| async { "posts" })));
}
let app = App::new()
.service(web::scope("/api/v1").configure(api_scope));
4. Extractors e requisições HTTP
4.1. Extração de dados
Axum - JSON:
use axum::{Json, extract::FromRequest};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> String {
format!("Created user: {} with email {}", payload.name, payload.email)
}
Actix-web - JSON:
use actix_web::{web, Responder, HttpResponse};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(payload: web::Json<CreateUser>) -> impl Responder {
HttpResponse::Created().json(serde_json::json!({
"name": payload.name,
"email": payload.email
}))
}
4.2. Validação de entrada
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3, max = 50))]
name: String,
#[validate(email)]
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
if let Err(errors) = payload.validate() {
return Err((StatusCode::BAD_REQUEST, errors.to_string()));
}
Ok(Json(serde_json::json!({"status": "created"})))
}
4.3. Custom extractors
Axum - FromRequest:
use axum::{
extract::{FromRequest, Request},
response::{Response, IntoResponse},
http::StatusCode,
};
struct AuthToken(String);
impl<S> FromRequest<S> for AuthToken
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let auth_header = req.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(|token| AuthToken(token.to_string()))
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "Missing token").into_response())?;
Ok(auth_header)
}
}
5. Respostas e serialização
5.1. Retornando JSON, HTML e streams
Axum:
use axum::{response::{Html, Json, Stream}, body::Body};
use futures::stream::{self, StreamExt};
async fn json_response() -> Json<serde_json::Value> {
Json(serde_json::json!({"message": "Hello"}))
}
async fn html_response() -> Html<&'static str> {
Html("<h1>Hello</h1>")
}
async fn stream_response() -> Stream<impl futures::Stream<Item = Result<String, std::io::Error>>> {
let stream = stream::iter(vec![
Ok("chunk1".to_string()),
Ok("chunk2".to_string()),
]);
Stream::new(stream)
}
5.2. Códigos de status e headers customizados
use axum::{
response::{Response, IntoResponse},
http::{StatusCode, HeaderMap, header},
};
async fn custom_response() -> Response {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
(StatusCode::CREATED, headers, "{\"id\": 1}").into_response()
}
5.3. Tratamento de erros tipados
use axum::{
response::{Response, IntoResponse},
http::StatusCode,
};
enum AppError {
NotFound(String),
Internal(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg).into_response(),
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response(),
}
}
}
async fn find_user(id: u32) -> Result<Json<serde_json::Value>, AppError> {
if id == 0 {
return Err(AppError::NotFound("User not found".to_string()));
}
Ok(Json(serde_json::json!({"id": id})))
}
6. Middleware e camadas intermediárias
6.1. Middleware de logging, CORS e compressão
Axum:
use tower_http::{cors::CorsLayer, trace::TraceLayer, compression::CompressionLayer};
let app = Router::new()
.route("/", get(|| async { "Hello" }))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new());
Actix-web:
use actix_cors::Cors;
use actix_web::middleware::Logger;
let app = App::new()
.wrap(Logger::default())
.wrap(Cors::permissive())
.route("/", web::get().to(|| async { "Hello" }));
6.2. Autenticação com JWT
use axum::{Router, routing::post, extract::State, Json};
use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
async fn login(Json(credentials): Json<serde_json::Value>) -> Json<serde_json::Value> {
let claims = Claims {
sub: credentials["username"].as_str().unwrap().to_string(),
exp: 10000000000,
};
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret("secret".as_ref())).unwrap();
Json(serde_json::json!({"token": token}))
}
6.3. Tower Service vs middleware chain
Axum utiliza o padrão Tower de layers, onde cada middleware é um Layer que envolve um Service. Isso permite composição funcional e reutilização de middlewares entre projetos. Actix-web usa uma cadeia de middlewares com wrap(), onde a ordem de aplicação é explícita e cada middleware pode modificar a requisição/resposta.
7. Testes e boas práticas
7.1. Testes de integração
Axum:
#[cfg(test)]
mod tests {
use axum::{Router, routing::get, body::Body, http::Request};
use tower::ServiceExt;
#[tokio::test]
async fn test_hello() {
let app = Router::new()
.route("/", get(|| async { "Hello" }));
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), 200);
}
}
Actix-web:
#[cfg(test)]
mod tests {
use actix_web::{test, web, App, Responder};
#[actix_web::test]
async fn test_hello() {
let app = test::init_service(
App::new().route("/", web::get().to(|| async { "Hello" }))
).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
}
7.2. Organização de projeto
src/
├── main.rs
├── config.rs
├── handlers/
│ ├── mod.rs
│ ├── users.rs
│ └── posts.rs
├── models/
│ ├── mod.rs
│ └── user.rs
├── middleware/
│ ├── mod.rs
│ └── auth.rs
└── errors.rs
7.3. Performance e graceful shutdown
use tokio::signal;
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
tokio::select! {
_ = ctrl_c => {},
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
8. Comparação final e conclusão
8.1. Resumo das vantagens e desvantagens
| Aspecto | Axum | Actix-web |
|---|---|---|
| Performance | Excelente | Superior em benchmarks |
| Ergonomia | API mais simples | Curva de aprendizado maior |
| Ecossistema | Integração com Tower |
| Ecossistema | Integração com Tower/gRPC | Middlewares maduros e crate actix |
| Maturidade | Relativamente novo mas estável | Muito maduro e testado em produção |
| Async runtime | Tokio (padrão) | Tokio (desde v4) |
| Documentação | Boa, mas em crescimento | Excelente e abundante |
8.2. Casos de uso recomendados
Escolha Axum quando:
- Você precisa de integração com gRPC (tonic) ou outros serviços baseados em Tower
- Prefere uma API mais enxuta e funcional
- O projeto já utiliza o ecossistema Tokio/Tower
- Valoriza a simplicidade e composição de middlewares
Escolha Actix-web quando:
- Performance de pico é crítica (benchmarks mostram vantagem em throughput)
- Você precisa de funcionalidades avançadas como WebSocket nativo e streaming
- O time já tem experiência com o framework
- Precisa de um ecossistema mais maduro de middlewares e extensões
8.3. Próximos passos
Após dominar os fundamentos destes frameworks, explore tópicos avançados como:
- Integração com banco de dados: SQLx, Diesel ou SeaORM para persistência
- Logging estruturado:
tracingcomtracing-subscriberpara observabilidade - Configuração: gerenciamento de variáveis de ambiente com
dotenveconfig - Deploy: Dockerização, health checks e balanceamento de carga
- Segurança: HTTPS com Let's Encrypt, rate limiting e proteção contra CSRF
Ambos os frameworks são excelentes escolhas para construir APIs HTTP em Rust. A decisão final depende das necessidades específicas do seu projeto, da familiaridade da equipe e dos requisitos de performance. Experimente ambos com exemplos simples e veja qual se adapta melhor ao seu fluxo de trabalho.