Cómo construir un servidor HTTP asíncrono en Rust con Tokio y Hyper
En este tutorial práctico crearás un servidor HTTP asíncrono minimalista y robusto en Rust usando tokio y hyper. Verás la estructura del proyecto, el código completo, pruebas básicas y por qué tomamos cada decisión.
Por qué Tokio + Hyper
- Tokio es el runtime asíncrono de facto en Rust: alto rendimiento y ecosistema maduro.
- Hyper es una librería HTTP de bajo nivel, muy eficiente y usada como base por frameworks como
warpoaxum. - Combinados, permiten un control fino sobre rendimiento y diseño arquitectural.
Requisitos
- Rust (stable)
- cargo
Estructura de carpetas
async-server/
├─ Cargo.toml
└─ src/
└─ main.rs
Cargo.toml
[package]
name = "async-server"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
hyper = "0.14"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Explicación rápida: tokio para runtime async; hyper como servidor HTTP; tracing para logs estructurados; serde/serde_json para JSON.
src/main.rs — Código completo
use std::convert::Infallible;
use hyper::{Body, Request, Response, Server, Method, StatusCode};
use hyper::service::{make_service_fn, service_fn};
use serde::Serialize;
use tracing::{info, error};
async fn handle(req: Request) -> Result, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => {
let body = "Hello from Rust + Hyper!";
Ok(Response::new(Body::from(body)))
}
(&Method::GET, "/json") => {
#[derive(Serialize)]
struct Msg { msg: &'static str }
let m = Msg { msg: "Hello JSON" };
let json = serde_json::to_string(&m).unwrap();
let mut res = Response::new(Body::from(json));
res.headers_mut().insert(
hyper::header::CONTENT_TYPE,
hyper::header::HeaderValue::from_static("application/json"),
);
Ok(res)
}
_ => {
let mut not_found = Response::new(Body::from("Not Found"));
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(not_found)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box> {
// Inicializa logging con nivel configurable vía RUST_LOG
tracing_subscriber::fmt::init();
let addr = ([127, 0, 0, 1], 3000).into();
// make_service crea una instancia de servicio por conexión
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(handle))
});
let server = Server::bind(&addr).serve(make_svc);
// Graceful shutdown al recibir Ctrl+C
let graceful = server.with_graceful_shutdown(async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl_c");
info!("Señal recibida, iniciando apagado ordenado");
});
info!("Escuchando en http://{}", addr);
if let Err(e) = graceful.await {
error!("error del servidor: {}", e);
}
Ok(())
}
Por qué este diseño
- service_fn: permite convertir una función async en un Service compatible con Hyper sin dependencias externas.
- make_service_fn: crea un servicio por conexión, útil si necesitas estado por conexión (ej. métricas, auth).
- with_graceful_shutdown: evita cortar conexiones abruptamente y permite liberar recursos.
- tracing en lugar de println: logs estructurados, filtrado por RUST_LOG y mejor para producción.
Ejecutar y probar
- Construir y ejecutar:
cargo run --release
- Probando con curl:
curl http://127.0.0.1:3000/
curl http://127.0.0.1:3000/json
curl -i http://127.0.0.1:3000/unknown
Buenas prácticas y errores comunes
- No hagas blocking I/O en handlers; si necesitas bloquear, muévelo a un
spawn_blocking.
- Maneja correctamente errores: evita
unwrap() en producción; usa tipos de error y mapea a respuestas HTTP.
- Para rutas y parsing complejo usa marcos de routing (axum, warp, tide) o implementa un router claro.
- Si necesitas middleware (autenticación, rate-limiting), considera
tower o migrar a frameworks basados en Tower.
Escalado y optimización
- Deploy: ejecuta con múltiples workers detrás de un load balancer o usa systemd/containers para instancias.
- HTTPS: agrega TLS con
rustls + hyper o usa un proxy inverso (NGINX, Caddy).
- Métricas: instrumenta con Prometheus (exportador) y usa
tracing + opentelemetry para trazas distribuidas.
- Conexiones y keep-alive: tunear timeouts y límites de conexión según tu carga.
Extensiones sugeridas
- Agrega un router con
axum para handlers más expresivos y extracción automática de parámetros.
- Integra autenticación JWT como middleware con
tower.
- Soporte HTTP/2 y gRPC mediante
tonic si necesitas RPC de alto rendimiento.
Consejo avanzado: para producción, reemplaza la función handler plana por una pila de middlewares (tower) que permita añadir logging correlacionado, timeouts y retries de forma composable. Como siguiente paso práctico integra TLS con rustls y añade pruebas de integración automatizadas que arranquen el servidor en background y realicen solicitudes reales.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación