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ística | Actix Web | Axum | Rocket | Warp |
|---|---|---|---|---|
| Performance | Excelente (top benchmarks) | Excelente | Boa | Muito boa |
| Maturidade | Muito madura (desde 2017) | Jovem (desde 2021) | Madura | Madura |
| API style | Macros + config | Funções + composição | Macros + atributos | Filtros compostos |
| Middleware | Próprio (Transform) | Tower (padrão) | Fairings | Filtros |
| Estado | web::Data (Arc) | State extractor | Managed state | Clone + Arc |
| Testes | Framework integrado | Via tower::ServiceExt | Framework integrado | Via warp::test |
| WebSocket | actix-ws | Integrado | Limitado | Integrado |
| Arquivos estáticos | actix-files | tower-http | Integrado | Via warp::fs |
| Templates | Tera, Askama | Tera, Askama | Tera, Handlebars | Tera, Askama |
| Ecossistema | Grande | Crescendo (Tower) | Moderado | Pequeno |
| Workers | Multi-worker nativo | Via Tokio | Via Tokio | Via 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::Datapara 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.