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: tracing com tracing-subscriber para observabilidade
  • Configuração: gerenciamento de variáveis de ambiente com dotenv e config
  • 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.