Actix-web lleva varios años en los primeros puestos de los benchmarks de TechEmpower. En la ronda 21, superaba a la mayoría de frameworks de cualquier lenguaje en peticiones por segundo. Hay razones técnicas detrás de eso: usa Tokio como runtime, tiene cero copias en la mayoría de rutas críticas y su sistema de extracción de datos de la petición está altamente optimizado.
Pero el rendimiento no es el único motivo por el que vale la pena conocerlo. Actix-web tiene una API madura, documentación extensa y un ecosistema activo con crates para sesiones, OAuth, WebSockets y más.
Para empezar:
[dependencies]
actix-web = "4"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
El servidor básico
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
async fn raiz() -> impl Responder {
HttpResponse::Ok().body("Hola desde Actix-web")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(raiz))
})
.bind("0.0.0.0:3000")?
.run()
.await
}
A diferencia de Axum, en Actix-web el App se construye dentro de una closure que se ejecuta una vez por hilo de trabajo. Esto permite que cada hilo tenga su propia copia del estado sin sincronización, lo que contribuye al rendimiento.
La macro #[actix_web::main] configura el runtime de Tokio con múltiples hilos. Por defecto usa tantos hilos como núcleos tenga la CPU.
Rutas y recursos
Actix-web tiene dos formas de definir rutas: con .route() (como Axum) o con .service() y recursos:
use actix_web::{web, App, HttpServer};
HttpServer::new(|| {
App::new()
// Estilo funcional
.route("/usuarios", web::get().to(listar))
.route("/usuarios", web::post().to(crear))
// Estilo recurso (agrupa rutas del mismo path)
.service(
web::resource("/usuarios/{id}")
.route(web::get().to(obtener))
.route(web::put().to(actualizar))
.route(web::delete().to(eliminar))
)
// Con scope (prefijo común)
.service(
web::scope("/api/v1")
.route("/productos", web::get().to(listar_productos))
)
})
Extractores de datos
Los extractores de Actix-web se parecen a los de Axum en concepto, pero usan tipos del crate actix-web:
Path: parámetros de ruta
use actix_web::web::Path;
async fn obtener(id: Path<u64>) -> impl Responder {
format!("Usuario {}", id.into_inner())
}
// Múltiples parámetros con una tupla o struct
#[derive(Deserialize)]
struct Params {
user_id: u64,
post_id: u64,
}
async fn obtener_post(params: Path<Params>) -> impl Responder {
format!("Post {} del usuario {}", params.post_id, params.user_id)
}
Query: parámetros de query string
use actix_web::web::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct Filtros {
pagina: Option<u32>,
busqueda: Option<String>,
}
async fn buscar(filtros: Query<Filtros>) -> impl Responder {
let pagina = filtros.pagina.unwrap_or(1);
let busqueda = filtros.busqueda.as_deref().unwrap_or("");
format!("Página {}, búsqueda: '{}'", pagina, busqueda)
}
Json: cuerpo de la petición
use actix_web::web::Json;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct NuevoUsuario {
nombre: String,
email: String,
}
#[derive(Serialize)]
struct Usuario {
id: u64,
nombre: String,
email: String,
}
async fn crear(datos: Json<NuevoUsuario>) -> impl Responder {
let usuario = Usuario {
id: 42,
nombre: datos.nombre.clone(),
email: datos.email.clone(),
};
web::Json(usuario)
}
Estado compartido con web::Data
En Actix-web el estado se registra con app_data y se extrae con web::Data:
use actix_web::web::Data;
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
type DB = Arc<Mutex<HashMap<u64, String>>>;
async fn listar(db: Data<DB>) -> impl Responder {
let db = db.lock().unwrap();
let usuarios: Vec<&String> = db.values().collect();
web::Json(usuarios.iter().map(|s| s.as_str()).collect::<Vec<_>>())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let db: DB = Arc::new(Mutex::new(HashMap::new()));
HttpServer::new(move || {
App::new()
.app_data(Data::new(db.clone()))
.route("/usuarios", web::get().to(listar))
})
.bind("0.0.0.0:3000")?
.run()
.await
}
Fíjate en el move en la closure de HttpServer::new: es necesario para capturar el db del scope exterior.
Middleware en Actix-web
Actix-web tiene su propio sistema de middleware. Hay middlewares incluidos en el crate actix-web y otros en actix-extras:
use actix_web::middleware::{Logger, NormalizePath, TrailingSlash};
HttpServer::new(|| {
App::new()
.wrap(Logger::default()) // Logging de peticiones
.wrap(NormalizePath::new(TrailingSlash::Trim)) // Normalizar rutas
.route("/", web::get().to(handler))
})
El logging de Actix-web usa el crate log. Para ver la salida necesitas un backend como env_logger:
[dependencies]
env_logger = "0.10"
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
Comparativa con Axum
Ambos frameworks son maduros y válidos para producción. Las diferencias principales son de diseño, no de capacidad:
Axum usa Tower para middlewares, lo que lo hace compatible con cualquier middleware del ecosistema Tower. Sus extractores son traits genéricos (FromRequest, FromRequestParts) y es relativamente fácil crear los propios. El estado se comparte con generics (Router<S>), lo que el compilador puede verificar totalmente.
Actix-web tiene su propio sistema de middlewares (aunque hay adaptadores para Tower). Es ligeramente más verboso en la configuración inicial pero tiene más utilidades incluidas de serie. Su modelo de múltiples hilos sin sincronización en el estado le da ventaja en benchmarks con estados locales por hilo.
// El mismo endpoint en Axum...
async fn crear_usuario(
State(db): State<DB>,
Json(datos): Json<NuevoUsuario>,
) -> (StatusCode, Json<Usuario>) { ... }
// ...y en Actix-web
async fn crear_usuario(
db: Data<DB>,
datos: Json<NuevoUsuario>,
) -> impl Responder { ... }
La diferencia de sintaxis es mínima. El código de negocio dentro del handler es idéntico.
Cuándo elegir cada uno
Para proyectos nuevos que usen Tokio como runtime y quieran integrarse con el ecosistema Tower (que es la mayoría), Axum es la elección natural. Su integración con tower-http y su sistema de estado tipado por el compilador reducen los errores en tiempo de ejecución.
Si ya tienes código en Actix-web, no hay motivo para migrar. Si tu principal preocupación es el rendimiento absoluto en benchmarks sintéticos con estado por hilo, Actix-web tiene ventaja en esos escenarios específicos.
En la práctica, el cuello de botella de la mayoría de APIs es la base de datos, no el framework web. Elige el que mejor se adapte a tu equipo y a las dependencias que ya usas.
