Axum es un framework web para Rust construido sobre Tokio, Hyper y Tower.
Sirve para crear APIs y servicios HTTP manteniendo lo que hace especial a Rust: tipos fuertes, errores explícitos y mucho control sin renunciar a una sintaxis bastante cómoda.
Crear el proyecto
Creamos un proyecto nuevo y añadimos las dependencias:
cargo new mi_api
cd mi_api
cargo add axum
cargo add tokio --features full
cargo add serde --features derive
cargo add serde_jsonaxum será el framework web, tokio el runtime asíncrono y serde nos permitirá convertir JSON a structs de Rust y al revés.
Nuestro primer Router
En Axum todo empieza con un Router.
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.await
.unwrap();
}
async fn root() -> &'static str {
"Hola desde Axum"
}Si ejecutamos cargo run y abrimos http://127.0.0.1:3000, veremos la respuesta.
En versiones modernas de Axum se usa tokio::net::TcpListener junto con axum::serve. Si encontráis ejemplos antiguos con axum::Server::bind, son de APIs anteriores.
Handlers
Un handler es la función que responde a una ruta.
async fn root() -> &'static str {
"Hola desde Axum"
}Puede devolver muchas cosas: texto, JSON, códigos HTTP, tuplas, tipos propios que implementen IntoResponse…
Eso hace que los handlers sean funciones Rust normales, sin tener que heredar de nada ni escribir una clase controlador.
Recibir JSON
Para recibir JSON usamos el extractor Json<T>.
use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CrearUsuario {
username: String,
email: String,
}
#[derive(Serialize)]
struct UsuarioRespuesta {
id: u64,
username: String,
mensaje: String,
}
async fn crear_usuario(
Json(payload): Json<CrearUsuario>,
) -> Json<UsuarioRespuesta> {
Json(UsuarioRespuesta {
id: 1,
username: payload.username,
mensaje: String::from("Usuario creado"),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/usuarios", post(crear_usuario));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}Si el cuerpo no es JSON válido o no encaja con CrearUsuario, el extractor rechaza la petición antes de llamar al handler. Los tipos forman parte de la validación estructural de entrada, aunque las reglas de negocio siguen siendo responsabilidad nuestra.
Parámetros de ruta
Para rutas como /usuarios/42, usamos Path.
use axum::extract::Path;
async fn obtener_usuario(Path(id): Path<u64>) -> String {
format!("Buscando usuario {}", id)
}
// En el router:
// .route("/usuarios/{id}", get(obtener_usuario))Axum intenta convertir el segmento {id} a u64. Si llega algo que no es un número, la petición no pasa al handler.
Estado compartido
Una API real necesita configuración, clientes HTTP, pools de base de datos o servicios compartidos.
Para eso usamos State:
use axum::{extract::State, routing::get, Router};
use std::sync::Arc;
struct AppState {
nombre: String,
}
async fn info(State(state): State<Arc<AppState>>) -> String {
format!("API: {}", state.nombre)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
nombre: String::from("Curso Rust"),
});
let app = Router::new()
.route("/info", get(info))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}El Arc permite co:::explain
mpartir la propiedad del estado entre peticiones. Si algún campo necesita mutar, tod
:::avía tendremos que elegir un mecanismo de sincronización adecuado.