rust-web-axum-api-rest

API REST en Rust con Axum: rutas, extractors y JSON

  • 3 min

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_json
Copied!

axum 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"
}
Copied!

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"
}
Copied!

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();
}
Copied!

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))
Copied!

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();
}
Copied!

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.