🦀 SSR "Desnudo" en Rust: Construye un micro-framework real con Axum + Askama
Inicia sesión para descargarTutorial paso a paso para entender el SSR “sin magia”: construyes un micro-framework en Rust con Axum + Askama, agregas layout reutilizable, SEO (title + meta), middleware (estáticos + compresión), errores 404/500 …
Contenido del tutorial ⌄
- SSR "Desnudo" en Rust
- Paso 0: ¿Qué vamos a construir?
- Paso 1: Templates de verdad (Layouts y Páginas)
- El Layout Base: Nuestro Molde Maestro
- Extendiendo el Layout: La Página de Inicio
- Paso 2: CSS sin framework (pero con estilo)
- Paso 3: El SSR en Rust (Askama en acción)
- Definiendo los Contratos de Datos (Structs)
- El Handler: Inyectando Datos en la Vista
- Paso 4: Un Router con alma de Framework (Middlewares)
- Paso 5: Configuración por Entorno (Dev vs. Prod)
SSR "Desnudo" en Rust
En frameworks como Next.js, el SSR parece magia pura: layouts, SEO y errores ya vienen incluidos. Pero, ¿qué pasa cuando la magia falla? Hoy en Tu Código Cotidiano vamos a "desnudar" el SSR. Construiremos desde cero un micro-framework en Rust para entender exactamente cómo se sirve HTML renderizado en el servidor.
No nos quedaremos en un simple "Hola Mundo". Este tutorial es un viaje hacia el control absoluto: configuraremos layouts reutilizables, manejadores de errores personalizados y una arquitectura de testing que te permitirá desplegar con absoluta confianza.
Entiende el enrutamiento, la inyección de templates y la gestión de assets estáticos sin cajas negras.
Implementa páginas de error 404/500 amigables y un sistema de configuración por entorno (Dev/Prod).
Middlewares eficientes con compresión Gzip/Brotli y control total sobre el ciclo de vida de la petición.
Paso 0: ¿Qué vamos a construir?
Antes de configurar el compilador de Rust, es crucial visualizar el destino. Vamos a construir un sistema capaz de servir HTML renderizado desde el servidor de forma inmediata, eliminando la necesidad de que el cliente procese megabytes de JavaScript innecesarios para visualizar una simple página.
Si levantas este servidor y exploras sus rutas, obtendrás una experiencia de navegación fluida, rápida y, sobre todo, controlada por ti:
GET /: Devuelve la página de inicio, construida y renderizada al instante en el servidor (SSR real).GET /no-existe: Atrapado elegantemente por nuestro sistema de fallback, devolviendo un error 404 estilizado y coherente con el diseño.GET /static/styles.css: Sirve nuestros estilos estéticos y optimizados (comprimidos al vuelo), garantizando que la carga sea extremadamente ligera.
El resultado es un sistema que no depende de "magia" externa ni de grandes frameworks pesados, sino de un código base que entiendes línea a línea. ¡Comencemos a armar las piezas!
Paso 1: Templates de verdad (Layouts y Páginas)
Lo primero que ve el usuario es la interfaz. En lugar de repetir la estructura HTML (el head, el header o el footer) en cada archivo, usaremos el sistema de herencia de Askama. Esto nos permite crear un "molde" base que todas las páginas seguirán, habilitando un SEO dinámico por página y facilitando un mantenimiento sencillo a largo plazo.
El Layout Base: Nuestro Molde Maestro
El archivo layout.html funciona como nuestro contenedor principal. Mediante el uso de {% block content %}{% endblock %}, definimos las zonas donde cada página específica inyectará su propio HTML. Esto nos permite mantener un diseño consistente sin duplicar ni una sola línea de código.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{{ title }}</title>
<meta name="description" content="{{ meta_description }}"/>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="/static/styles.css"/>
</head>
<body>
<header class="topbar">
<a class="brand" href="/">SSR Rust Desnudo</a>
<nav class="nav">
<a href="/">Inicio</a>
</nav>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<small>Hecho con Rust 🦀 + Axum + Askama</small>
</footer>
</body>
</html>
🔍 SEO Dinámico y Variables Tipadas
Las llaves {{ title }} y {{ meta_description }} son variables que inyectaremos desde Rust. A diferencia de lenguajes interpretados como Node.js o Python, en Rust + Askama estas variables son estrictamente tipadas en tiempo de compilación. Si omites enviar una de ellas desde tu backend, el servidor ni siquiera compilará, garantizando que nunca despliegues una página sin sus meta-etiquetas para Google.
🧩 El Motor de Herencia (Block Content)
La magia de este archivo radica en {% block content %}{% endblock %}. Esto actúa como un "molde" o placeholder. En lugar de copiar la barra de navegación y el footer en cada vista de tu app, las demás páginas solo tendrán que extender este layout y Askama inyectará su contenido exactamente en ese hueco. Así mantienes tu HTML 100% DRY (Don't Repeat Yourself).
🎨 Gestión de Assets Estáticos
Nota cómo el CSS y el Favicon apuntan a la ruta /static/.... Aunque en este momento solo es HTML, más adelante en el tutorial configuraremos un middleware (usando ServeDir) en el Router de Axum. Esto le indicará al servidor cómo servir estos archivos físicos al navegador web con compresión automática.
Extendiendo el Layout: La Página de Inicio
Ahora que tenemos nuestro molde base, crear una nueva vista es increíblemente limpio. El archivo index.html no necesita saber nada sobre etiquetas <head>, metadatos o footers. Simplemente le dice a Askama que herede la estructura principal y proporciona el contenido exacto que irá en el centro de la pantalla.
{% extends "layout.html" %}
{% block content %}
<div class="card">
<h1>¡Hola, {{ nombre }}! 👋</h1>
<p>Estás viendo SSR usando <strong>{{ framework }}</strong>.</p>
<p><small>SEO listo: title + meta description vienen del servidor.</small></p>
</div>
{% endblock %}
🧬 La Directiva extends
Al usar {% extends "layout.html" %} en la primera línea, le estamos indicando al motor de plantillas que esta no es una página independiente, sino un fragmento. Es el equivalente a construir componentes de UI modernos (como en React, Svelte o Vue), pero compilado a binario nativo y ejecutado en el servidor a velocidad récord.
🎯 Inyección Quirúrgica con block
Todo lo que escribas dentro de {% block content %}...{% endblock %} reemplazará automáticamente al bloque vacío con el mismo nombre que dejamos en nuestro layout.html. Puedes tener múltiples bloques en un layout (por ejemplo, un block scripts justo antes de cerrar el body para cargar JS específico solo en ciertas rutas).
🔒 Variables y Contrato de Datos
Aquí estamos usando {{ nombre }} y {{ framework }}, pero no olvides que estamos extendiendo el layout. Eso significa que la página final también exige title y meta_description del layout. Rust y Askama detectarán esto y nos obligarán a crear un Struct (un contrato de datos) que contenga todos los campos. Si olvidas uno solo, el compilador protegerá tu servidor bloqueando la compilación.
Paso 2: CSS sin framework (pero con estilo)
En el mundo moderno del desarrollo web, es muy fácil caer en la trampa de inyectar megabytes de CSS en línea o depender de librerías utilitarias masivas que ensucian el HTML. Aquí vamos a hacer las cosas a la vieja escuela, pero bien hechas: cero CSS en línea. Todo vivirá donde debe vivir.
Al separar nuestra hoja de estilos en la carpeta static/, le permitimos al navegador cachear este archivo de forma completamente independiente al HTML dinámico que generará nuestro servidor Rust. Esto significa que en la segunda visita de un usuario, el CSS se carga instantáneamente desde su disco local.
:root {
/* Palette */
--bg: #0b1220;
--bg2: #162447;
--card: rgba(255, 255, 255, 0.05);
--border: rgba(255, 255, 255, 0.10);
--text: #e7ecf5;
--muted: #a8b3c7;
/* Rust accent */
--accent: #dd4814;
/* Sizes */
--radius: 16px;
--radius-sm: 12px;
--container: 960px;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
color: var(--text);
background: radial-gradient(1200px 600px at 20% 10%, var(--bg2), var(--bg));
padding: 40px 16px;
}
/* Layout */
.topbar,
.container,
.footer {
max-width: var(--container);
margin-left: auto;
margin-right: auto;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.brand {
text-decoration: none;
color: var(--text);
font-weight: 800;
letter-spacing: 0.2px;
}
.nav a {
text-decoration: none;
color: var(--muted);
padding: 8px 10px;
border-radius: 10px;
}
.nav a:hover {
color: var(--text);
background: rgba(0, 0, 0, 0.18);
}
.container {
padding-top: 8px;
}
.footer {
margin-top: 18px;
color: var(--muted);
}
/* Components */
.card {
max-width: 720px;
margin: 0 auto;
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 22px;
box-shadow: 0 10px 30px rgba(0,0,0,0.35);
}
.card-error {
border-color: rgba(221, 72, 20, 0.35);
}
h1 {
margin: 0 0 10px;
font-size: 28px;
line-height: 1.2;
}
p { margin: 10px 0; }
strong { color: var(--accent); }
small { color: var(--muted); }
code {
padding: 2px 8px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.22);
border: 1px solid rgba(255,255,255,0.10);
}
/* Badge */
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(0,0,0,0.22);
font-size: 12px;
color: var(--muted);
margin-bottom: 12px;
}
/* Actions + Button */
.actions { margin-top: 16px; }
.btn {
display: inline-block;
text-decoration: none;
color: var(--text);
padding: 10px 14px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255,255,255,0.12);
background: rgba(0,0,0,0.22);
}
.btn:hover {
border-color: rgba(221, 72, 20, 0.45);
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
}
/* Responsive */
@media (max-width: 520px) {
body { padding: 28px 12px; }
.topbar { flex-direction: column; align-items: flex-start; }
h1 { font-size: 24px; }
.card { padding: 18px; }
}
💡 Mini Reto de Diseño
Agrega un estilo .brand:hover para darle vida al logotipo de la barra de navegación, o diseña un modo "error" con colores más marcados (jugando con la clase .card-error que ya dejamos preparada en el código). Más adelante, cuando implementemos la página 404, agradecerás tener estos estilos listos.
⚡ El impacto en la carga
Al tener un HTML muy limpio (sin miles de clases utilitarias de un framework) y un archivo CSS externo, el servidor Rust tendrá que enviar muchísimos menos bytes a través de la red por cada petición. Un payload más pequeño significa un First Contentful Paint (FCP) casi instantáneo.
Paso 3: El SSR en Rust (Askama en acción)
Una vez que tenemos la interfaz y los estilos listos, es hora de dar vida al Server-Side Rendering. En este paso abordaremos el núcleo SSR, donde ocurre la verdadera magia: conectaremos los datos puros de Rust con las vistas HTML que acabamos de crear.
Nos centraremos en dos archivos clave: src/templates/mod.rs, donde definiremos los Structs (como HolaTemplate) que actúan como el contrato de datos para alimentar a index.html, y src/routes/mod.rs, donde crearemos el handler de Axum. Veremos cómo la función render() de Askama produce un String a partir de la plantilla, y cómo Axum inyecta automáticamente los headers correctos para servirlo como Html<String>. ¡Al terminar este paso, verás cómo el SEO básico (title y meta_description) sale 'cocinado' directamente desde el servidor!
Definiendo los Contratos de Datos (Structs)
En Askama, la conexión entre tu backend y tu frontend HTML se hace mediante Structs. Cada estructura representa una página, y sus campos corresponden exactamente a las variables {{ variable }} que dejamos en nuestros archivos .html. Si añades una variable en el HTML y olvidas ponerla en el struct (o te equivocas en el nombre), Rust se negará a compilar. ¡Adiós a los errores de "undefined" en producción!
//! Templates Askama (SSR).
//! Askama busca por defecto en la carpeta `templates/` en la raíz del proyecto.
use askama::Template;
#[derive(Template)]
#[template(path = "index.html")]
pub struct HolaTemplate<'a> {
pub title: &'a str,
pub meta_description: &'a str,
pub nombre: &'a str,
pub framework: &'a str,
}
#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate {
pub title: String,
pub meta_description: String,
pub status_code: u16,
pub message: String,
pub path: String,
pub show_path: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hola_template_renderiza_y_contiene_campos_clave() {
let nombre = "Felipe";
let framework = "Rust + Askama";
let t = HolaTemplate {
title: "Inicio",
meta_description: "Demo SSR con Rust",
nombre,
framework,
};
let html = t.render().expect("el template debería renderizar");
// T05.1 (DoD):
assert!(
html.contains("Hola,") || html.contains("¡Hola,"),
"Debe contener el saludo 'Hola,'"
);
assert!(html.contains(nombre), "Debe contener el nombre");
assert!(html.contains(framework), "Debe contener el framework");
// Extras útiles (no estorban)
assert!(html.contains("<html"));
assert!(html.contains("<title>Inicio</title>"));
}
#[test]
fn error_template_renderiza() {
let t = ErrorTemplate {
title: "Error".to_string(),
meta_description: "Error en el servidor".to_string(),
status_code: 500,
message: "Algo salió mal".to_string(),
path: "/x".to_string(),
show_path: true,
};
let html = t.render().expect("el template de error debería renderizar");
assert!(html.contains("500"));
assert!(html.contains("Algo salió mal"));
assert!(html.contains("<title>Error</title>"));
assert!(html.contains("Error en el servidor"));
assert!(html.contains("/x"));
}
}
🚀 Lifetimes vs. Strings: Rendimiento extremo
Fíjate en HolaTemplate<'a>. Estamos usando referencias de texto (&'a str) en lugar de String. Esto es una optimización brutal en Rust: significa que no estamos copiando memoria. Al inyectar el texto estático directamente en el HTML, el rendimiento es casi idéntico al de servir un archivo estático. En ErrorTemplate usamos String porque los mensajes de error suelen generarse dinámicamente en tiempo de ejecución.
🛡️ Tests Unitarios Integrados
El bloque #[cfg(test)] es otra maravilla del ecosistema Rust. Nos permite escribir las pruebas en el mismo archivo. Aquí comprobamos que el método render() efectivamente genera un HTML válido que contiene nuestras variables inyectadas. Así aseguramos que la vista se arma correctamente antes de siquiera levantar el servidor Axum.
🔗 El Atributo #[template(...)]
Con #[template(path = "index.html")], le estamos diciendo al macro de Askama dónde ir a buscar el HTML correspondiente. En tiempo de compilación, Askama leerá ese archivo HTML y generará código Rust ultrarrápido equivalente a concatenar strings de forma segura.
El Handler: Inyectando Datos en la Vista
En Axum, un handler es simplemente una función asíncrona que responde a una petición HTTP. Aquí es donde instanciamos nuestro HolaTemplate con datos reales. Luego llamamos al método render() (que Askama generó por nosotros) para convertir esa estructura en un bloque sólido de texto HTML.
#![forbid(unsafe_code)]
use askama::Template;
use axum::{http::Uri, response::Html};
use crate::errors::AppError;
use crate::{errors::AppResult, templates::HolaTemplate};
pub async fn index() -> AppResult<Html<String>> {
let tmpl = HolaTemplate {
title: "SSR con Rust + Askama",
meta_description: "Micro-framework SSR: Axum + Askama, layout base y errores SSR.",
nombre: "Lector de Tu Código Cotidiano",
framework: "Rust + Askama",
};
let body = tmpl.render().map_err(AppError::from)?;
Ok(Html(body))
}
pub async fn not_found(uri: Uri) -> AppError {
AppError::not_found(uri.path())
}
📦 El contenedor Html<String>
Podríamos devolver un simple String, pero Axum lo enviaría al navegador como texto plano (text/plain). Al envolver nuestra respuesta en Html(body), Axum automáticamente inyecta el header Content-Type: text/html; charset=utf-8. Así el navegador sabe que debe pintar la página web y no solo mostrar texto.
❓ El manejo de errores con '?'
La línea tmpl.render().map_err(AppError::from)? es la forma elegante en Rust de decir: "Intenta renderizar esto. Si falla, conviértelo en mi error personalizado (AppError) y aborta la función devolviendo ese error". En breve crearemos ese AppError para que los fallos también devuelvan HTML bonito.
🚧 La ruta not_found
La función not_found(uri: Uri) capturará cualquier petición a una ruta que no hayamos definido (como /una-ruta-rara) y disparará un error 404. Gracias a que toma la Uri original, podremos decirle al usuario exactamente qué ruta fue la que falló.
Paso 4: Un Router con alma de Framework (Middlewares)
Para que este proyecto pase de ser un experimento a sentirse como un framework real, necesitamos herramientas de producción. Es hora de darle superpoderes a nuestro enrutador.
Nos enfocaremos en src/app.rs. Aquí construiremos la función build_router(). Separar la creación del router de la inicialización del servidor es una práctica "Pro" que hace que nuestra aplicación sea modular y, lo más importante, 100% testeable. A este enrutador le conectaremos capas (middlewares) para manejar logs, compresión, archivos estáticos y errores de rutas no encontradas.
#![forbid(unsafe_code)]
use axum::{
Router,
extract::MatchedPath,
http::{Request, Response, StatusCode},
routing::get,
};
use std::time::Duration;
use tower_http::{compression::CompressionLayer, services::ServeDir, trace::TraceLayer};
use tracing::{Level, debug, error, info};
use tracing_subscriber::EnvFilter;
use crate::config::{AppConfig, AppEnv};
fn init_tracing(env: AppEnv) {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
let level = if env.is_dev() { "debug" } else { "info" };
EnvFilter::new(level)
});
tracing_subscriber::fmt().with_env_filter(filter).init();
}
// Evita ensuciar logs con 404 del navegador
async fn favicon() -> StatusCode {
StatusCode::NO_CONTENT
}
/// Router real de la app (usado por `serve()` y por tests).
pub fn build_router() -> Router {
let trace_layer = TraceLayer::new_for_http()
.make_span_with(|req: &Request<_>| {
let matched_path = req
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str)
.unwrap_or("<unmatched>");
tracing::span!(
Level::INFO,
"http_request",
method = %req.method(),
uri = %req.uri(),
matched_path = matched_path,
)
})
.on_response(
|res: &Response<_>, latency: Duration, _span: &tracing::Span| {
tracing::info!(
status = %res.status(),
latency_ms = latency.as_millis(),
"http_response"
);
},
);
let static_dir = ServeDir::new("static");
Router::new()
.route("/", get(crate::routes::index))
.route("/favicon.ico", get(favicon))
.nest_service("/static", static_dir)
.fallback(crate::routes::not_found)
.layer(trace_layer)
.layer(CompressionLayer::new())
}
pub async fn serve() {
let cfg = AppConfig::from_env();
init_tracing(cfg.env);
debug!("AppConfig cargada: {:?}", cfg);
let app = build_router();
let addr = cfg.addr();
info!("🚀 Servidor listo en http://{addr} (env={})", cfg.env);
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
error!("❌ No se pudo bindear {addr}: {e}. Tip: prueba PORT=3001 cargo run");
return;
}
};
if let Err(e) = axum::serve(listener, app).await {
error!("❌ El servidor falló: {e}");
}
}
#[cfg(test)]
mod tests {
use super::build_router;
use axum::{body::Body, http::Request, http::StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt; // <- oneshot()
#[tokio::test]
async fn ruta_raiz_status_200_y_body_esperado() {
let app = build_router();
let req = Request::builder()
.uri("/")
.body(Body::empty())
.expect("request debe construir");
let res = app.oneshot(req).await.expect("router debe responder");
// DoD T05.2: status 200
assert_eq!(res.status(), StatusCode::OK);
// DoD T05.2: body contiene fragmento esperado
let bytes = res
.into_body()
.collect()
.await
.expect("body debe colectarse")
.to_bytes();
let body = String::from_utf8(bytes.to_vec()).expect("body debe ser UTF-8");
assert!(
body.contains("¡Hola,") || body.contains("Hola,"),
"debe contener saludo"
);
assert!(body.contains("Rust + Askama"), "debe contener framework");
}
}
🛡️ TraceLayer y Middlewares de Torre (Tower)
El ecosistema tower_http nos provee piezas de infraestructura estándar. Nuestro TraceLayer inspecciona cada petición entrante y saliente, midiendo automáticamente la latencia (milisegundos) y registrando el método, la ruta y el código de estado (200, 404, 500). Es observabilidad gratuita lista para producción.
🗜️ Rendimiento Extremo (Compresión y Estáticos)
Al aplicar CompressionLayer::new() y ServeDir::new("static"), nuestro micro-framework ahora despacha todo nuestro CSS e imágenes pre-comprimidas (Gzip o Brotli, según lo que soporte el navegador). Esto rivaliza directamente con la eficiencia de servidores dedicados como Nginx.
🧹 Fallback y Favicon Nulo: Manteniendo los logs limpios
El método .fallback() captura cualquier ruta que no coincida con nada (nuestro 404 SSR). Además, los navegadores siempre piden /favicon.ico. Al crear una ruta específica que devuelve un estado 204 No Content, evitamos que nuestro servidor lance errores 404 innecesarios que ensuciarían nuestra consola de logs.
✅ Testeabilidad Nativa (oneshot)
Observa el final del archivo. Gracias a separar build_router(), en nuestro bloque de tests podemos crear una petición HTTP simulada, enviarla al enrutador con oneshot(req) y leer el HTML resultante. ¡Todo ocurre en memoria! No hace falta abrir puertos ni levantar el servidor de verdad para testear que el SSR funciona a la perfección.
Paso 5: Configuración por Entorno (Dev vs. Prod)
Un proyecto real no se comporta igual en la computadora del desarrollador que en el servidor de producción. Necesitamos un mecanismo robusto para que la aplicación se adapte.
En el archivo src/config.rs vamos a crear un sistema que lea variables de entorno (como APP_ENV=dev|prod o PORT=8080). Esto nos permitirá, por ejemplo, tener logs sumamente detallados (nivel debug) mientras programamos para encontrar errores, pero mantener un registro limpio y enfocado solo en peticiones críticas (nivel info) cuando el sistema esté desplegado, ahorrando así espacio en disco y mejorando el rendimiento.
#![forbid(unsafe_code)]
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppEnv {
Dev,
Prod,
}
impl AppEnv {
pub fn is_dev(self) -> bool {
matches!(self, Self::Dev)
}
}
impl std::str::FromStr for AppEnv {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"dev" | "development" => Ok(Self::Dev),
"prod" | "production" => Ok(Self::Prod),
_ => Err(()),
}
}
}
impl std::fmt::Display for AppEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Dev => write!(f, "dev"),
Self::Prod => write!(f, "prod"),
}
}
}
#[derive(Debug, Clone)]
pub struct AppConfig {
pub host: IpAddr,
pub port: u16,
pub env: AppEnv,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
host: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 3000,
env: AppEnv::Dev,
}
}
}
impl AppConfig {
/// Lee HOST, PORT y APP_ENV del entorno con defaults seguros.
/// - HOST: default 127.0.0.1
/// - PORT: default 3000
/// - APP_ENV: default dev (valores: dev|prod)
pub fn from_env() -> Self {
let mut cfg = Self::default();
if let Some(ip) = std::env::var("HOST")
.ok()
.and_then(|v| v.parse::<IpAddr>().ok())
{
cfg.host = ip;
}
if let Some(p) = std::env::var("PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
{
cfg.port = p;
}
if let Some(e) = std::env::var("APP_ENV")
.ok()
.and_then(|v| v.parse::<AppEnv>().ok())
{
cfg.env = e;
}
cfg
}
pub fn addr(&self) -> SocketAddr {
SocketAddr::new(self.host, self.port)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_ok() {
let cfg = AppConfig::default();
assert_eq!(cfg.host, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(cfg.port, 3000);
assert_eq!(cfg.env, AppEnv::Dev);
}
#[test]
fn parse_env_variants() {
assert_eq!("dev".parse::<AppEnv>().unwrap(), AppEnv::Dev);
assert_eq!("production".parse::<AppEnv>().unwrap(), AppEnv::Prod);
assert!("unknown".parse::<AppEnv>().is_err());
}
}
Comentarios y valoraciones
No hay comentarios aún. ¡Sé el primero en opinar!