Actix Web Rust: Framework de Alta Performance | Rust Brasil

Guia do Actix Web em Rust: rotas, extractors, middleware, WebSockets e templates. O framework web mais rápido do ecossistema.

O Actix Web é um dos frameworks web mais rápidos do mundo, consistentemente figurando no topo dos benchmarks TechEmpower. Construído sobre o runtime Tokio, ele oferece uma API ergonômica e madura para construir aplicações web e APIs HTTP, com suporte completo a WebSockets, middleware, templates e arquivos estáticos.

O nome vem do Actix, um framework de atores para Rust que originou o projeto. Embora o Actix Web moderno não dependa mais diretamente do sistema de atores para o core da funcionalidade web, ele herda conceitos como isolamento de estado e processamento concorrente que contribuem para sua performance excepcional.

O Actix Web é uma escolha estabelecida no ecossistema Rust, usado em produção por empresas como Microsoft, Samsung e diversas startups. Se você precisa de máxima performance e um framework web maduro com ecossistema rico, o Actix Web é uma excelente opção.

Instalação

Adicione o Actix Web e dependências ao Cargo.toml:

[dependencies]
actix-web = "4"
actix-rt = "2"
actix-files = "0.6"           # Arquivos estáticos
actix-multipart = "0.7"       # Upload de arquivos
actix-ws = "0.3"              # WebSockets
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
env_logger = "0.11"
log = "0.4"

# 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 actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn raiz() -> impl Responder {
    HttpResponse::Ok().body("Olá, mundo!")
}

#[get("/ola/{nome}")]
async fn saudacao(path: web::Path<String>) -> impl Responder {
    let nome = path.into_inner();
    HttpResponse::Ok().body(format!("Olá, {}!", nome))
}

#[post("/echo")]
async fn echo(body: String) -> impl Responder {
    HttpResponse::Ok().body(body)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Inicializar logger
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

    log::info!("Servidor iniciando em http://localhost:8080");

    HttpServer::new(|| {
        App::new()
            .service(raiz)
            .service(saudacao)
            .service(echo)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

Rotas e Configuração

use actix_web::{web, App, HttpServer, HttpResponse, Responder};

// Handlers como funções normais (sem macro)
async fn pagina_inicial() -> impl Responder {
    HttpResponse::Ok().body("Página inicial")
}

async fn sobre() -> impl Responder {
    HttpResponse::Ok().body("Sobre nós")
}

// Configuração modular de rotas
fn configurar_api(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api/v1")
            .route("/usuarios", web::get().to(listar_usuarios))
            .route("/usuarios", web::post().to(criar_usuario))
            .route("/usuarios/{id}", web::get().to(obter_usuario))
            .route("/usuarios/{id}", web::put().to(atualizar_usuario))
            .route("/usuarios/{id}", web::delete().to(deletar_usuario))
    );
}

fn configurar_produtos(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api/v1/produtos")
            .route("", web::get().to(listar_produtos))
            .route("", web::post().to(criar_produto))
            .route("/{id}", web::get().to(obter_produto))
    );
}

fn configurar_admin(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/admin")
            .route("/dashboard", web::get().to(dashboard))
            .route("/usuarios", web::get().to(admin_usuarios))
    );
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            // Rotas simples
            .route("/", web::get().to(pagina_inicial))
            .route("/sobre", web::get().to(sobre))
            // Configuração modular
            .configure(configurar_api)
            .configure(configurar_produtos)
            .configure(configurar_admin)
            // Rota padrão (404)
            .default_service(web::to(|| async {
                HttpResponse::NotFound().body("Página não encontrada")
            }))
    })
    .bind(("0.0.0.0", 8080))?
    .workers(4)  // Número de workers (padrão: núm. de CPUs)
    .run()
    .await
}

// Handlers placeholder
async fn listar_usuarios() -> impl Responder { HttpResponse::Ok().body("[]") }
async fn criar_usuario() -> impl Responder { HttpResponse::Created().finish() }
async fn obter_usuario() -> impl Responder { HttpResponse::Ok().finish() }
async fn atualizar_usuario() -> impl Responder { HttpResponse::Ok().finish() }
async fn deletar_usuario() -> impl Responder { HttpResponse::NoContent().finish() }
async fn listar_produtos() -> impl Responder { HttpResponse::Ok().body("[]") }
async fn criar_produto() -> impl Responder { HttpResponse::Created().finish() }
async fn obter_produto() -> impl Responder { HttpResponse::Ok().finish() }
async fn dashboard() -> impl Responder { HttpResponse::Ok().body("Dashboard") }
async fn admin_usuarios() -> impl Responder { HttpResponse::Ok().body("Admin") }

Request Extractors

use actix_web::{get, post, put, web, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};

// === Path: parâmetros da URL ===
#[get("/usuarios/{id}")]
async fn obter_usuario(path: web::Path<u64>) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().body(format!("Usuário ID: {}", id))
}

// Múltiplos parâmetros com struct
#[derive(Deserialize)]
struct PostPath {
    user_id: u64,
    post_id: u64,
}

#[get("/usuarios/{user_id}/posts/{post_id}")]
async fn obter_post(path: web::Path<PostPath>) -> impl Responder {
    HttpResponse::Ok().body(format!(
        "Usuário {} - Post {}",
        path.user_id, path.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>,
    categoria: Option<String>,
}

fn pagina_padrao() -> u32 { 1 }
fn limite_padrao() -> u32 { 20 }

// GET /produtos?pagina=2&limite=10&busca=notebook
#[get("/produtos")]
async fn listar_produtos(query: web::Query<Paginacao>) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "pagina": query.pagina,
        "limite": query.limite,
        "busca": query.busca,
        "categoria": query.categoria,
    }))
}

// === 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,
}

#[post("/usuarios")]
async fn criar_usuario(payload: web::Json<CriarUsuarioRequest>) -> impl Responder {
    let usuario = UsuarioResponse {
        id: 1,
        nome: payload.nome.clone(),
        email: payload.email.clone(),
        ativo: payload.ativo,
    };

    HttpResponse::Created().json(usuario)
}

// === Headers e Request completo ===
async fn ver_headers(req: HttpRequest) -> impl Responder {
    let user_agent = req
        .headers()
        .get("User-Agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("desconhecido");

    let content_type = req
        .headers()
        .get("Content-Type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("não especificado");

    HttpResponse::Ok().json(serde_json::json!({
        "user_agent": user_agent,
        "content_type": content_type,
        "method": req.method().as_str(),
        "uri": req.uri().to_string(),
    }))
}

// === Form data (x-www-form-urlencoded) ===
#[derive(Deserialize)]
struct LoginForm {
    email: String,
    senha: String,
}

#[post("/login")]
async fn login(form: web::Form<LoginForm>) -> impl Responder {
    // Verificar credenciais (simplificado)
    if form.email == "admin@exemplo.com" && form.senha == "123456" {
        HttpResponse::Ok().json(serde_json::json!({"token": "abc123"}))
    } else {
        HttpResponse::Unauthorized().json(serde_json::json!({"erro": "Credenciais inválidas"}))
    }
}

// === Combinando múltiplos extractors ===
#[put("/usuarios/{id}")]
async fn atualizar_usuario(
    path: web::Path<u64>,
    payload: web::Json<CriarUsuarioRequest>,
    req: HttpRequest,
) -> impl Responder {
    let id = path.into_inner();
    let auth = req.headers().get("Authorization");

    match auth {
        Some(_) => {
            HttpResponse::Ok().json(serde_json::json!({
                "id": id,
                "nome": payload.nome,
                "atualizado": true,
            }))
        }
        None => HttpResponse::Unauthorized().finish(),
    }
}

Recursos Avançados

State Management

use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use std::sync::Mutex;

// Estado compartilhado entre todos os workers
struct AppState {
    nome_app: String,
    contador: Mutex<u64>,
}

async fn status(data: web::Data<AppState>) -> impl Responder {
    let contagem = data.contador.lock().unwrap();
    HttpResponse::Ok().json(serde_json::json!({
        "app": data.nome_app,
        "requisicoes": *contagem,
    }))
}

async fn incrementar(data: web::Data<AppState>) -> impl Responder {
    let mut contagem = data.contador.lock().unwrap();
    *contagem += 1;
    HttpResponse::Ok().json(serde_json::json!({"contagem": *contagem}))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // web::Data é Arc internamente, compartilhado entre workers
    let app_state = web::Data::new(AppState {
        nome_app: "MeuApp".to_string(),
        contador: Mutex::new(0),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .route("/status", web::get().to(status))
            .route("/incrementar", web::post().to(incrementar))
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

Middleware

use actix_web::{
    dev::{Service, ServiceRequest, ServiceResponse, Transform},
    web, App, Error, HttpMessage, HttpResponse, HttpServer,
    middleware,
};
use std::future::{self, Ready, Future};
use std::pin::Pin;
use std::time::Instant;
use log;

// === Middleware customizado: Timer ===

pub struct Timer;

impl<S, B> Transform<S, ServiceRequest> for Timer
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = TimerMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        future::ready(Ok(TimerMiddleware { service }))
    }
}

pub struct TimerMiddleware<S> {
    service: S,
}

impl<S, B> Service<ServiceRequest> for TimerMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

    fn poll_ready(
        &self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let inicio = Instant::now();
        let metodo = req.method().to_string();
        let path = req.path().to_string();

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;
            let tempo = inicio.elapsed();
            log::info!(
                "{} {} -> {} ({:?})",
                metodo,
                path,
                res.status(),
                tempo
            );
            Ok(res)
        })
    }
}

// === Usando middleware built-in e customizado ===

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

    HttpServer::new(|| {
        // CORS
        let cors = actix_web::middleware::DefaultHeaders::new()
            .add(("Access-Control-Allow-Origin", "*"))
            .add(("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"))
            .add(("Access-Control-Allow-Headers", "Content-Type, Authorization"));

        App::new()
            // Logger built-in
            .wrap(middleware::Logger::default())
            // Comprimir respostas
            .wrap(middleware::Compress::default())
            // Headers padrão
            .wrap(cors)
            // Middleware customizado
            .wrap(Timer)
            // Normalizar path (trailing slash)
            .wrap(middleware::NormalizePath::trim())
            .route("/", web::get().to(|| async {
                HttpResponse::Ok().body("OK")
            }))
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

Tratamento de Erros

use actix_web::{
    error, get, web, App, HttpResponse, HttpServer,
    http::StatusCode,
};
use serde::Serialize;
use thiserror::Error;
use std::fmt;

// Erro customizado 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")]
    Interno(#[from] anyhow::Error),
}

#[derive(Serialize)]
struct ErroResponse {
    codigo: u16,
    mensagem: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    detalhes: Option<String>,
}

// Implementar ResponseError para converter em HTTP responses
impl error::ResponseError for AppError {
    fn status_code(&self) -> StatusCode {
        match self {
            AppError::NaoEncontrado(_) => StatusCode::NOT_FOUND,
            AppError::Validacao(_) => StatusCode::BAD_REQUEST,
            AppError::NaoAutorizado => StatusCode::UNAUTHORIZED,
            AppError::Interno(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn error_response(&self) -> HttpResponse {
        let status = self.status_code();

        let (mensagem, detalhes) = match self {
            AppError::NaoEncontrado(recurso) => (
                "Recurso não encontrado".to_string(),
                Some(recurso.clone()),
            ),
            AppError::Validacao(msg) => (
                "Dados inválidos".to_string(),
                Some(msg.clone()),
            ),
            AppError::NaoAutorizado => (
                "Não autorizado".to_string(),
                None,
            ),
            AppError::Interno(err) => {
                log::error!("Erro interno: {:?}", err);
                (
                    "Erro interno do servidor".to_string(),
                    None,
                )
            }
        };

        HttpResponse::build(status).json(ErroResponse {
            codigo: status.as_u16(),
            mensagem,
            detalhes,
        })
    }
}

// Usar nos handlers
#[get("/usuarios/{id}")]
async fn obter_usuario(path: web::Path<u64>) -> Result<HttpResponse, AppError> {
    let id = path.into_inner();

    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(HttpResponse::Ok().json(serde_json::json!({
        "id": id,
        "nome": "Maria Silva"
    })))
}

// Configurar tamanho máximo de JSON
fn configurar_json(cfg: &mut web::ServiceConfig) {
    let json_config = web::JsonConfig::default()
        .limit(4096) // 4KB máximo
        .error_handler(|err, _req| {
            let resposta = ErroResponse {
                codigo: 400,
                mensagem: "JSON inválido".to_string(),
                detalhes: Some(err.to_string()),
            };
            let response = HttpResponse::BadRequest().json(resposta);
            error::InternalError::from_response(err, response).into()
        });

    cfg.app_data(json_config);
}

WebSocket

use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_ws;

async fn ws_handler(
    req: HttpRequest,
    stream: web::Payload,
) -> Result<HttpResponse, Error> {
    let (response, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;

    // Spawn handler para processar mensagens
    actix_web::rt::spawn(async move {
        while let Some(Ok(msg)) = msg_stream.recv().await {
            match msg {
                actix_ws::Message::Text(texto) => {
                    log::info!("WS recebido: {}", texto);
                    // Echo
                    let resposta = format!("Você disse: {}", texto);
                    if session.text(resposta).await.is_err() {
                        break;
                    }
                }
                actix_ws::Message::Ping(bytes) => {
                    if session.pong(&bytes).await.is_err() {
                        break;
                    }
                }
                actix_ws::Message::Close(reason) => {
                    let _ = session.close(reason).await;
                    break;
                }
                _ => {}
            }
        }
    });

    Ok(response)
}

// Registrar no App
fn configurar_websocket(cfg: &mut web::ServiceConfig) {
    cfg.route("/ws", web::get().to(ws_handler));
}

Arquivos Estáticos e Templates

use actix_files as fs;
use actix_web::{web, App, HttpServer, HttpResponse};

fn configurar_estaticos(cfg: &mut web::ServiceConfig) {
    // Servir diretório inteiro
    cfg.service(
        fs::Files::new("/static", "./static")
            .show_files_listing()  // Listar arquivos (dev only)
            .index_file("index.html")
            .use_etag(true)
            .use_last_modified(true)
    );

    // Servir um arquivo específico
    cfg.route("/favicon.ico", web::get().to(|| async {
        fs::NamedFile::open_async("./static/favicon.ico").await
    }));
}

// Template simples com string interpolation
async fn pagina_html(path: web::Path<String>) -> HttpResponse {
    let nome = path.into_inner();
    let html = format!(r#"
        <!DOCTYPE html>
        <html lang="pt-BR">
        <head>
            <meta charset="UTF-8">
            <title>Olá, {nome}</title>
            <link rel="stylesheet" href="/static/style.css">
        </head>
        <body>
            <h1>Olá, {nome}!</h1>
            <p>Bem-vindo à nossa aplicação Actix Web.</p>
        </body>
        </html>
    "#);

    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

Boas Práticas

1. Organize com Configuração Modular

// src/routes/mod.rs
mod usuarios;
mod produtos;
mod auth;

use actix_web::web;

pub fn configurar(cfg: &mut web::ServiceConfig) {
    cfg.configure(auth::configurar);
    cfg.service(
        web::scope("/api/v1")
            .configure(usuarios::configurar)
            .configure(produtos::configurar)
    );
}
// src/routes/usuarios.rs
use actix_web::{web, HttpResponse, Responder};

pub fn configurar(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/usuarios")
            .route("", web::get().to(listar))
            .route("", web::post().to(criar))
            .route("/{id}", web::get().to(obter))
            .route("/{id}", web::put().to(atualizar))
            .route("/{id}", web::delete().to(deletar))
    );
}

async fn listar() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!([]))
}

async fn criar() -> impl Responder {
    HttpResponse::Created().finish()
}

async fn obter(path: web::Path<u64>) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({"id": path.into_inner()}))
}

async fn atualizar(path: web::Path<u64>) -> impl Responder {
    HttpResponse::Ok().finish()
}

async fn deletar(path: web::Path<u64>) -> impl Responder {
    HttpResponse::NoContent().finish()
}

2. Use Guards para Controle de Acesso

use actix_web::{guard, web, App, HttpResponse, HttpServer};

fn configurar_com_guards(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            // Apenas aceitar JSON
            .guard(guard::Header("content-type", "application/json"))
            .route("/dados", web::post().to(|| async {
                HttpResponse::Ok().body("Dados JSON recebidos")
            }))
    );

    cfg.service(
        web::scope("/admin")
            // Apenas requests de localhost
            .guard(guard::fn_guard(|ctx| {
                ctx.head()
                    .peer_addr
                    .map(|addr| addr.ip().is_loopback())
                    .unwrap_or(false)
            }))
            .route("/", web::get().to(|| async {
                HttpResponse::Ok().body("Admin")
            }))
    );
}

3. Configure Limites e Timeouts

use actix_web::{web, App, HttpServer};
use std::time::Duration;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        // Limitar tamanho do corpo JSON
        let json_config = web::JsonConfig::default()
            .limit(1024 * 1024);  // 1MB

        // Limitar tamanho de payload geral
        let payload_config = web::PayloadConfig::default()
            .limit(10 * 1024 * 1024);  // 10MB

        App::new()
            .app_data(json_config)
            .app_data(payload_config)
    })
    .bind(("0.0.0.0", 8080))?
    .workers(4)
    .keep_alive(Duration::from_secs(75))
    .client_request_timeout(Duration::from_secs(60))
    .client_disconnect_timeout(Duration::from_secs(5))
    .shutdown_timeout(30)  // Tempo para graceful shutdown
    .run()
    .await
}

4. Testes de Integração

#[cfg(test)]
mod tests {
    use actix_web::{test, web, App, HttpResponse};
    use serde_json::json;

    // Handler de teste
    async fn criar_usuario(
        payload: web::Json<serde_json::Value>,
    ) -> HttpResponse {
        HttpResponse::Created().json(json!({
            "id": 1,
            "nome": payload["nome"],
        }))
    }

    #[actix_web::test]
    async fn teste_criar_usuario() {
        // Criar app de teste
        let app = test::init_service(
            App::new().route("/usuarios", web::post().to(criar_usuario))
        ).await;

        // Criar requisição
        let req = test::TestRequest::post()
            .uri("/usuarios")
            .set_json(json!({
                "nome": "Maria",
                "email": "maria@exemplo.com"
            }))
            .to_request();

        // Executar e verificar
        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), 201);

        // Verificar corpo
        let body: serde_json::Value = test::read_body_json(resp).await;
        assert_eq!(body["nome"], "Maria");
        assert_eq!(body["id"], 1);
    }

    #[actix_web::test]
    async fn teste_rota_nao_encontrada() {
        let app = test::init_service(
            App::new()
                .route("/", web::get().to(|| async { HttpResponse::Ok().finish() }))
        ).await;

        let req = test::TestRequest::get()
            .uri("/inexistente")
            .to_request();

        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), 404);
    }

    #[actix_web::test]
    async fn teste_com_state() {
        let app = test::init_service(
            App::new()
                .app_data(web::Data::new("estado_teste".to_string()))
                .route("/", web::get().to(|data: web::Data<String>| async move {
                    HttpResponse::Ok().body(data.into_inner().to_string())
                }))
        ).await;

        let req = test::TestRequest::get().uri("/").to_request();
        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), 200);

        let body = test::read_body(resp).await;
        assert_eq!(body, "estado_teste");
    }
}

5. Graceful Shutdown

use actix_web::{web, App, HttpServer, HttpResponse};
use tokio::signal;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let server = HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(|| async {
                HttpResponse::Ok().body("OK")
            }))
    })
    .bind(("0.0.0.0", 8080))?
    .shutdown_timeout(30)  // 30 segundos para finalizar requests
    .run();

    // Obter handle para shutdown
    let handle = server.handle();

    // Spawn task para escutar sinais
    tokio::spawn(async move {
        signal::ctrl_c().await.unwrap();
        log::info!("Sinal de shutdown recebido, encerrando...");
        handle.stop(true).await;
    });

    server.await
}

Exemplos Práticos

Exemplo: Aplicação Web Completa

use actix_web::{get, post, put, delete, web, App, HttpServer, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
use uuid::Uuid;
use chrono::{DateTime, Utc};

// === Modelos ===

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Tarefa {
    id: String,
    titulo: String,
    descricao: Option<String>,
    concluida: bool,
    prioridade: Prioridade,
    criada_em: DateTime<Utc>,
    atualizada_em: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Prioridade {
    Baixa,
    Media,
    Alta,
    Urgente,
}

#[derive(Debug, Deserialize)]
struct CriarTarefaRequest {
    titulo: String,
    descricao: Option<String>,
    #[serde(default = "prioridade_padrao")]
    prioridade: Prioridade,
}

fn prioridade_padrao() -> Prioridade {
    Prioridade::Media
}

#[derive(Debug, Deserialize)]
struct AtualizarTarefaRequest {
    titulo: Option<String>,
    descricao: Option<String>,
    concluida: Option<bool>,
    prioridade: Option<Prioridade>,
}

#[derive(Debug, Deserialize)]
struct FiltrosTarefa {
    #[serde(default)]
    concluida: Option<bool>,
    prioridade: Option<String>,
    busca: Option<String>,
}

#[derive(Serialize)]
struct ResumoDashboard {
    total: usize,
    concluidas: usize,
    pendentes: usize,
    por_prioridade: HashMap<String, usize>,
}

// === Estado ===

struct AppState {
    tarefas: Mutex<HashMap<String, Tarefa>>,
}

// === Handlers ===

#[post("/api/tarefas")]
async fn criar_tarefa(
    data: web::Data<AppState>,
    payload: web::Json<CriarTarefaRequest>,
) -> impl Responder {
    // Validação
    if payload.titulo.trim().is_empty() {
        return HttpResponse::BadRequest().json(serde_json::json!({
            "erro": "Título é obrigatório"
        }));
    }

    let agora = Utc::now();
    let tarefa = Tarefa {
        id: Uuid::new_v4().to_string(),
        titulo: payload.titulo.clone(),
        descricao: payload.descricao.clone(),
        concluida: false,
        prioridade: payload.prioridade.clone(),
        criada_em: agora,
        atualizada_em: agora,
    };

    let mut tarefas = data.tarefas.lock().unwrap();
    tarefas.insert(tarefa.id.clone(), tarefa.clone());

    HttpResponse::Created().json(tarefa)
}

#[get("/api/tarefas")]
async fn listar_tarefas(
    data: web::Data<AppState>,
    filtros: web::Query<FiltrosTarefa>,
) -> impl Responder {
    let tarefas = data.tarefas.lock().unwrap();

    let mut resultado: Vec<&Tarefa> = tarefas.values()
        .filter(|t| {
            // Filtro de status
            if let Some(concluida) = filtros.concluida {
                if t.concluida != concluida {
                    return false;
                }
            }
            // Filtro de busca
            if let Some(ref busca) = filtros.busca {
                let busca = busca.to_lowercase();
                if !t.titulo.to_lowercase().contains(&busca) {
                    if let Some(ref desc) = t.descricao {
                        if !desc.to_lowercase().contains(&busca) {
                            return false;
                        }
                    } else {
                        return false;
                    }
                }
            }
            true
        })
        .collect();

    // Ordenar por data de criação (mais recente primeiro)
    resultado.sort_by(|a, b| b.criada_em.cmp(&a.criada_em));

    HttpResponse::Ok().json(resultado)
}

#[get("/api/tarefas/{id}")]
async fn obter_tarefa(
    data: web::Data<AppState>,
    path: web::Path<String>,
) -> impl Responder {
    let id = path.into_inner();
    let tarefas = data.tarefas.lock().unwrap();

    match tarefas.get(&id) {
        Some(tarefa) => HttpResponse::Ok().json(tarefa),
        None => HttpResponse::NotFound().json(serde_json::json!({
            "erro": format!("Tarefa '{}' não encontrada", id)
        })),
    }
}

#[put("/api/tarefas/{id}")]
async fn atualizar_tarefa(
    data: web::Data<AppState>,
    path: web::Path<String>,
    payload: web::Json<AtualizarTarefaRequest>,
) -> impl Responder {
    let id = path.into_inner();
    let mut tarefas = data.tarefas.lock().unwrap();

    match tarefas.get_mut(&id) {
        Some(tarefa) => {
            if let Some(ref titulo) = payload.titulo {
                tarefa.titulo = titulo.clone();
            }
            if let Some(ref descricao) = payload.descricao {
                tarefa.descricao = Some(descricao.clone());
            }
            if let Some(concluida) = payload.concluida {
                tarefa.concluida = concluida;
            }
            if let Some(ref prioridade) = payload.prioridade {
                tarefa.prioridade = prioridade.clone();
            }
            tarefa.atualizada_em = Utc::now();

            HttpResponse::Ok().json(tarefa.clone())
        }
        None => HttpResponse::NotFound().json(serde_json::json!({
            "erro": format!("Tarefa '{}' não encontrada", id)
        })),
    }
}

#[delete("/api/tarefas/{id}")]
async fn deletar_tarefa(
    data: web::Data<AppState>,
    path: web::Path<String>,
) -> impl Responder {
    let id = path.into_inner();
    let mut tarefas = data.tarefas.lock().unwrap();

    match tarefas.remove(&id) {
        Some(_) => HttpResponse::NoContent().finish(),
        None => HttpResponse::NotFound().json(serde_json::json!({
            "erro": format!("Tarefa '{}' não encontrada", id)
        })),
    }
}

#[get("/api/dashboard")]
async fn dashboard(data: web::Data<AppState>) -> impl Responder {
    let tarefas = data.tarefas.lock().unwrap();

    let total = tarefas.len();
    let concluidas = tarefas.values().filter(|t| t.concluida).count();
    let pendentes = total - concluidas;

    let mut por_prioridade: HashMap<String, usize> = HashMap::new();
    for tarefa in tarefas.values() {
        let chave = format!("{:?}", tarefa.prioridade).to_lowercase();
        *por_prioridade.entry(chave).or_insert(0) += 1;
    }

    HttpResponse::Ok().json(ResumoDashboard {
        total,
        concluidas,
        pendentes,
        por_prioridade,
    })
}

#[get("/saude")]
async fn health_check() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "status": "ok",
        "timestamp": Utc::now().to_rfc3339(),
    }))
}

// === Aplicação Principal ===

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

    let app_state = web::Data::new(AppState {
        tarefas: Mutex::new(HashMap::new()),
    });

    log::info!("Servidor rodando em http://localhost:8080");

    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            // Middleware
            .wrap(actix_web::middleware::Logger::default())
            .wrap(actix_web::middleware::Compress::default())
            .wrap(actix_web::middleware::NormalizePath::trim())
            // Rotas
            .service(health_check)
            .service(dashboard)
            .service(criar_tarefa)
            .service(listar_tarefas)
            .service(obter_tarefa)
            .service(atualizar_tarefa)
            .service(deletar_tarefa)
    })
    .bind(("0.0.0.0", 8080))?
    .workers(4)
    .shutdown_timeout(30)
    .run()
    .await
}

Comparação com Alternativas

CaracterísticaActix WebAxumRocketWarp
PerformanceExcelente (top benchmarks)ExcelenteBoaMuito boa
MaturidadeMuito madura (desde 2017)Jovem (desde 2021)MaduraMadura
API styleMacros + configFunções + composiçãoMacros + atributosFiltros compostos
MiddlewarePróprio (Transform)Tower (padrão)FairingsFiltros
Estadoweb::Data (Arc)State extractorManaged stateClone + Arc
TestesFramework integradoVia tower::ServiceExtFramework integradoVia warp::test
WebSocketactix-wsIntegradoLimitadoIntegrado
Arquivos estáticosactix-filestower-httpIntegradoVia warp::fs
TemplatesTera, AskamaTera, AskamaTera, HandlebarsTera, Askama
EcossistemaGrandeCrescendo (Tower)ModeradoPequeno
WorkersMulti-worker nativoVia TokioVia TokioVia Tokio

O Actix Web se destaca por:

  • Performance de referência: consistentemente entre os frameworks web mais rápidos do mundo
  • Maturidade: anos de uso em produção com API estável
  • Multi-worker: cada worker tem seu próprio runtime Tokio
  • Framework de testes integrado: testar handlers é muito simples
  • Ecossistema rico: plugins para multipart, sessions, identity, rate limiting

Conclusão

O Actix Web é um framework web maduro, extremamente performático e com um ecossistema rico para Rust. Sua API baseada em macros e configuração modular torna fácil organizar aplicações de qualquer tamanho, desde APIs simples até sistemas complexos em produção.

Pontos-chave para lembrar:

  • App::new() para construir a aplicação com middleware e rotas
  • Extractors (Path, Query, Json, Form) para extrair dados de requisições
  • web::Data para compartilhar estado entre handlers e workers
  • Macros de rota (#[get], #[post], etc.) para handlers declarativos
  • configure() para organização modular de rotas
  • Framework de testes integrado para testes de integração
  • Guards para controle de acesso baseado em condições

Para aprofundar, consulte a documentação oficial do Actix Web e os exemplos no GitHub.

Se prefere uma abordagem mais moderna baseada no ecossistema Tower/Tokio, confira o Axum.