Axum 0.8: construir APIs HTTP en Rust en 2026

Si llevas un tiempo con Rust y quieres construir una API HTTP, lo más probable es que hayas acabado mirando Axum. No es casualidad: lo mantiene el mismo equipo detrás de Tokio, se apoya en Tower para el middleware y en Hyper para el transporte HTTP, y la versión 0.8 (lanzada a finales de 2024) dejó la ergonomía bastante mejor que antes. En 2026 es el framework más descargado de crates.io dentro de la categoría web, y la encuesta anual State of Rust lo confirma año tras año.

Esta guía parte de cero, pero asume que ya sabes escribir Rust. No vamos a detenernos en qué es un trait ni en cómo funciona el ownership; sí vamos a ver cómo montar un servidor real con rutas, extractores, estado compartido y tests.

Por qué Axum en 2026

Hay varias opciones en Rust para hacer HTTP. Actix-web es el más antiguo y maduro, usa una arquitectura actor y en los benchmarks sintéticos de TechEmpower suele aparecer en los primeros puestos de todos los lenguajes. Warp apuesta por la composición de filtros, que resulta elegante pero puede hacerse difícil de depurar. Rocket es el más opinado: usa macros de procedimiento para las rutas y tiene su propia forma de hacer las cosas.

Axum toma un camino distinto: no inventa abstracciones propias si ya existen en Tower. Eso significa que cualquier middleware escrito para Tower funciona directamente con Axum, y hay decenas de ellos: autenticación, rate limiting, compresión, trazado... La integración con Tokio también es total, porque comparten equipo y filosofía de diseño. Para proyectos nuevos en 2026, la mayoría de la comunidad elige Axum precisamente por eso: no porque sea "el mejor" en abstracto, sino porque encaja bien con el resto del ecosistema async de Rust.

También puedes leer la historia de Rust en el backend y por qué muchas empresas lo adoptan si quieres contexto más amplio antes de entrar en el framework.

Estructura básica de un servidor Axum

Añade las dependencias en Cargo.toml:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

El servidor mínimo tiene este aspecto:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn root_handler() -> &'static str {
    "Hola desde Axum"
}

axum::serve reemplaza al antiguo Server::bind que venía de Hyper directamente. Es un cambio de la 0.7 a la 0.8 que simplifica bastante el punto de entrada. Los handlers son funciones async que devuelven cualquier tipo que implemente IntoResponse: un string, una tupla (StatusCode, String), un Json<T>, lo que necesites.

Extractores: cómo Axum lee la request

La parte más característica de Axum son los extractores. En lugar de pasarte un objeto Request y que tú lo parsees, declaras qué necesitas como argumentos de la función y Axum se encarga de extraerlo. Si falla la extracción, devuelve un 400 o un 422 automáticamente.

Los más usados:

  • Path<T>: parámetros de la URL. Con la ruta /users/:id, el handler recibe Path(id): Path<u32>.
  • Query<T>: query string. Con ?page=2&limit=10 y un struct que derive Deserialize, obtienes el struct ya deserializado.
  • Json<T>: body JSON. Axum lo lee, lo deserializa con serde y te lo entrega. Si el JSON no es válido, responde 422.
  • State<T>: estado compartido entre handlers (base de datos, configuración, clientes HTTP...).
  • Headers: acceso a las cabeceras HTTP de la request.

Un handler que usa varios a la vez:

use axum::extract::{Path, Query, Json};
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    limit: Option<u32>,
}

async fn list_users(
    Query(pagination): Query<Pagination>,
) -> Json<serde_json::Value> {
    let page = pagination.page.unwrap_or(1);
    let limit = pagination.limit.unwrap_or(20);
    Json(serde_json::json!({ "page": page, "limit": limit }))
}

El orden de los argumentos importa cuando hay dependencias entre extractores, pero en la mayoría de los casos puedes ponerlos en el orden que te resulte más legible.

State: compartir la conexión a la base de datos

Pasar un pool de base de datos a todos los handlers es uno de los casos más comunes. Con axum::extract::State queda limpio y sin globales:

use axum::{extract::State, routing::get, Router};
use sqlx::PgPool;

#[derive(Clone)]
struct AppState {
    db: PgPool,
}

async fn get_users(State(state): State<AppState>) -> String {
    // state.db ya está disponible aquí
    format!("Pool disponible")
}

#[tokio::main]
async fn main() {
    let pool = PgPool::connect("postgres://localhost/mydb")
        .await
        .unwrap();

    let state = AppState { db: pool };

    let app = Router::new()
        .route("/users", get(get_users))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

El estado tiene que implementar Clone, pero PgPool de sqlx ya lo hace internamente con un Arc, así que clonar el pool no duplica conexiones. La ventaja frente a un global estático es que el compilador sabe exactamente qué handlers necesitan qué estado, y puedes pasar estados distintos a subrouters distintos.

Middlewares con Tower

Axum no tiene su propio sistema de middleware: usa Tower directamente. Eso significa que puedes usar cualquier Layer de Tower o de crates como tower-http:

use axum::Router;
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer};

let app = Router::new()
    .route("/", get(root_handler))
    .layer(
        ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(CorsLayer::permissive()),
    );

tower-http incluye middleware para CORS, compresión, autenticación básica, timeout, límite de tamaño de body y trazado con tracing. Para middlewares propios, Axum ofrece axum::middleware::from_fn, que te permite escribir una función async sin implementar el trait Layer a mano:

use axum::{middleware, response::Response, extract::Request};

async fn auth_middleware(
    request: Request,
    next: middleware::Next,
) -> Response {
    // comprueba cabecera Authorization, por ejemplo
    next.run(request).await
}

Para casos más complejos donde necesitas estado dentro del middleware, ahí sí tiene sentido implementar Layer y Service directamente.

Manejo de errores

Por defecto, si un handler devuelve Err(...) y el error no implementa IntoResponse, el compilador se queja. La forma más limpia es definir un tipo de error propio:

use axum::{http::StatusCode, response::{IntoResponse, Response}};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("no encontrado")]
    NotFound,
    #[error("error de base de datos: {0}")]
    Database(#[from] sqlx::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status = match &self {
            AppError::NotFound => StatusCode::NOT_FOUND,
            AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
        };
        (status, self.to_string()).into_response()
    }
}

Con esto, los handlers pueden devolver Result<Json<T>, AppError> y usar ? para propagar errores de sqlx u otras operaciones. El código de los handlers queda limpio: solo lógica de negocio, sin bloques match para cada posible fallo.

Para las rutas que no existen, Axum devuelve un 404 genérico. Si quieres personalizarlo, añade un fallback al router:

let app = Router::new()
    .route("/users", get(list_users))
    .fallback(handler_404);

async fn handler_404() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "Esta ruta no existe")
}

Testing de handlers sin levantar un servidor TCP

Una de las mejoras que trajo Axum 0.8 es axum::test::TestClient, que te permite hacer peticiones HTTP directamente al router sin abrir ningún puerto:

use axum::{body::Body, http::{Request, StatusCode}};
use tower::ServiceExt;

#[tokio::test]
async fn test_root_handler() {
    let app = Router::new().route("/", get(root_handler));

    let response = app
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
}

Con la API de TestClient la sintaxis es más directa todavía:

use axum_test::TestServer;

#[tokio::test]
async fn test_con_test_server() {
    let app = build_router();
    let server = TestServer::new(app).unwrap();

    let response = server.get("/users/1").await;
    assert_eq!(response.status_code(), StatusCode::OK);
}

Puedes pasar estado al router igual que en producción, así que los tests de integración funcionan con la misma base de código sin adaptaciones.

Axum vs Actix-web en 2026

La pregunta aparece en casi todos los foros de Rust. La respuesta corta: los dos son buenas opciones y los dos están activamente mantenidos.

Actix-web lleva más años en producción, tiene más ejemplos y la arquitectura actor puede encajar bien en ciertos tipos de carga. En los benchmarks de TechEmpower, tanto Actix-web como Axum aparecen en el top 5 de todos los frameworks web, de todos los lenguajes. La diferencia de rendimiento en cargas reales es marginal y raramente será el cuello de botella de tu aplicación.

Axum gana en ergonomía cuando ya usas Tokio y sqlx, que es el stack async más extendido en Rust. La integración es directa, sin adaptadores, y el sistema de extractores resulta más fácil de extender que el sistema de guards de Actix. También puedes ver una comparativa con Go para servicios web si estás decidiendo entre lenguajes antes de comprometerte con uno.

Para proyectos nuevos en 2026 donde el equipo no tiene experiencia previa con ninguno de los dos, Axum suele ser la recomendación por defecto de la comunidad. No porque Actix-web sea peor, sino porque el stack Tokio + sqlx + Axum está muy bien documentado y los errores del compilador son más fáciles de interpretar.

Siguientes pasos

Con lo que has visto aquí tienes suficiente para montar una API funcional con autenticación, acceso a base de datos y tests. Los siguientes temas que merece la pena explorar son: WebSockets con axum::extract::WebSocketUpgrade, streaming de respuestas con Body personalizado, y despliegue con Docker aprovechando la compilación estática que permite Rust con musl. La documentación oficial en docs.rs/axum tiene ejemplos para cada uno de ellos, y el repositorio en GitHub incluye una carpeta examples/ con casos de uso concretos.

Imagen: Pexels / luis gomes

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP