{"id":147,"date":"2026-02-07T16:29:55","date_gmt":"2026-02-07T20:29:55","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=147"},"modified":"2026-02-16T11:04:34","modified_gmt":"2026-02-16T15:04:34","slug":"full-stack-en-rust-estructurando-aplicaciones-con-leptos","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/02\/full-stack-en-rust-estructurando-aplicaciones-con-leptos\/","title":{"rendered":"Arquitectura Full-Stack en Rust: Estructurando aplicaciones con Leptos sin morir en el intento"},"content":{"rendered":"\n<p>Si vienes del mundo de React, Next.js o Go, sabes que mantener la sincron\u00eda entre el frontend y el backend es una de las mayores fuentes de bugs. Rust, con el framework <strong><a href=\"https:\/\/leptos.dev\/\" data-type=\"link\" data-id=\"https:\/\/leptos.dev\/\">Leptos<\/a><\/strong>, propone una alternativa interesante: <strong>isomorfismo real<\/strong>. Un mismo lenguaje, tipos compartidos y una \u00fanica definici\u00f3n central de los datos y reglas del sistema.<\/p>\n\n\n\n<p>Esta filosof\u00eda no es nueva para quienes venimos de <a href=\"https:\/\/juredev.com\/blog\/tag\/c\/\" data-type=\"link\" data-id=\"https:\/\/juredev.com\/blog\/tag\/c\/\"><strong>C#<\/strong><\/a>, donde compartir librer\u00edas de modelos entre el servidor y el cliente es una pr\u00e1ctica habitual para mantener la consistencia de los datos. Sin embargo, Leptos aplica este enfoque de forma especialmente natural al desarrollo web moderno, permitiendo que esa \u201cfuente \u00fanica de verdad\u201d se compile tanto en el binario del servidor como en el WebAssembly del navegador.<\/p>\n\n\n\n<p>El resultado es un modelo en el que los contratos entre frontend y backend se expresan directamente en el c\u00f3digo compartido, reduciendo la necesidad de definiciones externas y disminuyendo los errores derivados de desalineaciones entre capas.<\/p>\n\n\n\n<p>En este art\u00edculo vamos a ver c\u00f3mo estructurar una aplicaci\u00f3n full-stack en Leptos con una arquitectura clara, pensada para ser escalable y mantenible, usando un ejemplo did\u00e1ctico orientado a introducir los conceptos principales del framework.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. El Coraz\u00f3n: La Capa de Dominio (\/domain)<\/h2>\n\n\n\n<p>En Leptos, el dominio puede ser <strong>isom\u00f3rfico<\/strong>. Esto significa que el c\u00f3digo dentro de esta carpeta se compila tanto para el servidor (nativo) como para el cliente (WebAssembly).<\/p>\n\n\n\n<p>Aqu\u00ed definimos nuestros <strong>structs<\/strong> y <strong>enums<\/strong>. Al usar la crate <strong>validator<\/strong>, podemos expresar reglas de negocio en un solo lugar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ domain\/user.rs\n#&#91;derive(Serialize, Deserialize, Validate)]\npub struct LoginCredentials {\n    #&#91;validate(length(min = 3, message = \"Usuario debe tener al menos 3 caracteres\"))]\n    pub username: String,\n    #&#91;validate(length(min = 6, message = \"Contrase\u00f1a debe tener al menos 6 caracteres\"))]\n    pub password: String,\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>\u00bfPor qu\u00e9 esto es clave?<\/strong><\/h3>\n\n\n\n<p>Porque la misma validaci\u00f3n se ejecuta en el frontend antes de realizar la request, evitando llamadas innecesarias, y vuelve a ejecutarse en el backend como validaci\u00f3n final. Esto ayuda a reducir la duplicaci\u00f3n de reglas entre capas y a mantener la consistencia del modelo.<\/p>\n\n\n\n<p>No elimina toda la l\u00f3gica espec\u00edfica de cada lado (por ejemplo, c\u00f3mo se muestran los errores en la UI), pero s\u00ed centraliza la definici\u00f3n de las reglas principales.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Estado Global Tipado<\/h3>\n\n\n\n<p>Adem\u00e1s de las validaciones, el dominio tambi\u00e9n define el estado de autenticaci\u00f3n de forma expl\u00edcita:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ domain\/state.rs\npub enum AuthState {\n    Loading,\n    Authenticated(User),\n    Unauthenticated,\n}<\/code><\/pre>\n\n\n\n<p>Este <strong>enum<\/strong> ayuda a evitar estados inv\u00e1lidos. No puedes representar un usuario autenticado sin datos, ni olvidarte de manejar el estado de carga. El compilador te obliga a ser exhaustivo, lo que reduce errores a medida que la aplicaci\u00f3n crece.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. La Fortaleza: El Servidor (\/server)<\/h2>\n\n\n\n<p>Aqu\u00ed reside la l\u00f3gica sensible: conexiones a bases de datos (SQLite\/SQLx), hashing de contrase\u00f1as (bcrypt) y gesti\u00f3n de sesiones.<\/p>\n\n\n\n<p>Gracias a las <strong>Server Functions<\/strong> de Leptos, no es necesario escribir una API REST manualmente. Simplemente defines una funci\u00f3n y Leptos se encarga del mecanismo de llamada entre cliente y servidor:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#&#91;server(Login, \"\/api\")]\npub async fn login(credentials: LoginCredentials) -&gt; Result&lt;User, ServerFnError&gt; {\n    \/\/ Validaci\u00f3n en servidor (misma l\u00f3gica que el cliente)\n    validate(&amp;credentials)?;\n    \n    \/\/ Consulta a la base de datos\n    let user = sqlx::query_as(\"SELECT * FROM users WHERE username = ?\")\n        .bind(&amp;credentials.username)\n        .fetch_optional(pool)\n        .await?;\n    \n    \/\/ Verificaci\u00f3n con bcrypt\n    bcrypt::verify(&amp;credentials.password, &amp;user.password_hash)?;\n    \n    \/\/ Creaci\u00f3n de sesi\u00f3n con UUID\n    let session_id = Uuid::new_v4().to_string();\n    \n    \/\/ Cookie HttpOnly con SameSite=Lax\n    let cookie = Cookie::build((SESSION_COOKIE_NAME, session_id))\n        .http_only(true)\n        .same_site(SameSite::Lax)\n        .max_age(Duration::days(7))\n        .build();\n    \n    Ok(user)\n}<\/code><\/pre>\n\n\n\n<p>Este ejemplo est\u00e1 <strong>simplificado a prop\u00f3sito<\/strong> para ilustrar el flujo general. Detalles como persistencia avanzada de sesiones, manejo fino de errores o protecciones adicionales se dejan fuera para mantener el foco en la arquitectura y en el modelo mental de Leptos.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Seguridad por Defecto<\/h3>\n\n\n\n<p>Al usar la directiva <em>[cfg(feature = \u00abssr\u00bb)]<\/em>, el c\u00f3digo del servidor solo se compila cuando la aplicaci\u00f3n se ejecuta en modo SSR. El resultado es que la l\u00f3gica sensible y las dependencias del backend <strong>no forman parte del bundle WebAssembly<\/strong> que se env\u00eda al navegador.<\/p>\n\n\n\n<p>Adem\u00e1s, la gesti\u00f3n de sesiones utiliza:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Cookies HttpOnly<\/strong>: JavaScript no puede acceder a ellas<\/li>\n\n\n\n<li><strong>SameSite=Lax<\/strong>: Protecci\u00f3n b\u00e1sica contra CSRF<\/li>\n\n\n\n<li><strong>Expiraci\u00f3n autom\u00e1tica<\/strong>: Las sesiones tienen un tiempo de vida definido<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">3. La Interfaz: Frontend Modular (\/frontend)<\/h2>\n\n\n\n<p>Para evitar que la UI se convierta en un conjunto desordenado de archivos, el frontend se organiza por <strong>features<\/strong>, no por categor\u00edas gen\u00e9ricas:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>frontend\/\n\u251c\u2500\u2500 auth\/\n\u251c\u2500\u2500 users\/\n\u251c\u2500\u2500 layouts\/\n\u2514\u2500\u2500 state\/<\/code><\/pre>\n\n\n\n<p>Este enfoque facilita el crecimiento del proyecto y ayuda a que cada parte tenga responsabilidades claras, algo especialmente \u00fatil cuando el c\u00f3digo base empieza a crecer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Contexto Global con Signals<\/h3>\n\n\n\n<p>El estado de autenticaci\u00f3n se gestiona mediante un <strong>Context Provider<\/strong> que envuelve toda la aplicaci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#&#91;component]\npub fn AuthProvider(children: Children) -&gt; impl IntoView {\n    let state = RwSignal::new(AuthState::Loading);\n    \n    let login_action = Action::new(|credentials| async {\n        login(credentials).await\n    });\n    \n    \/\/ Efecto reactivo: cuando login_action cambia, actualiza el estado\n    Effect::new(move |_| {\n        if let Some(Ok(user)) = login_action.value().get() {\n            state.set(AuthState::Authenticated(user));\n        }\n    });\n    \n    provide_context(AuthContext { state, login_action });\n    children()\n}<\/code><\/pre>\n\n\n\n<p>Cualquier componente puede acceder al estado con <strong>use_auth()<\/strong>, sin necesidad de prop drilling.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. El Flujo de Trabajo (Workflow)<\/h2>\n\n\n\n<p>Uno de los puntos fuertes de Leptos es su <strong>reactividad de grano fino<\/strong>. En lugar de re-renderizar componentes completos, el framework actualiza directamente los nodos del DOM que dependen de un valor concreto.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ejemplo: Flujo de Login<\/h3>\n\n\n\n<p>1. <strong>El usuario env\u00eda el LoginForm<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Validaci\u00f3n en cliente con <strong>validator<\/strong><\/li>\n\n\n\n<li>Feedback inmediato si falla<\/li>\n<\/ul>\n\n\n\n<p>2. <strong>Una Action de Leptos llama a la Server Function<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Serializaci\u00f3n autom\u00e1tica<\/li>\n\n\n\n<li>POST a <strong>\/api\/login<\/strong><\/li>\n<\/ul>\n\n\n\n<p>3. <strong>El servidor procesa la l\u00f3gica<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Consulta SQLite con <strong>sqlx<\/strong><\/li>\n\n\n\n<li>Verifica el hash con <strong>bcrypt<\/strong><\/li>\n\n\n\n<li>Crea sesi\u00f3n y cookie<\/li>\n<\/ul>\n\n\n\n<p>4. <strong>La UI reacciona actualizando el estado global<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El <strong>Effect<\/strong> detecta el cambio<\/li>\n\n\n\n<li>Se actualiza <strong>AuthState<\/strong><\/li>\n\n\n\n<li>Los componentes dependientes reaccionan autom\u00e1ticamente<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Ventajas sobre React\/Next.js<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Aspecto<\/th><th>React \/ Next.js<\/th><th>Leptos<\/th><\/tr><\/thead><tbody><tr><td><strong>Validaci\u00f3n<\/strong><\/td><td>Normalmente duplicada<\/td><td>Definici\u00f3n central reutilizable<\/td><\/tr><tr><td><strong>Tipado<\/strong><\/td><td>TypeScript<\/td><td>Rust (compile-time)<\/td><\/tr><tr><td><strong>Re-renders<\/strong><\/td><td>Por componente<\/td><td>Por nodo dependiente<\/td><\/tr><tr><td><strong>Integraci\u00f3n FE\/BE<\/strong><\/td><td>Contratos impl\u00edcitos<\/td><td>Tipos compartidos<\/td><\/tr><tr><td><strong>Errores de integraci\u00f3n<\/strong><\/td><td>Detectados en runtime<\/td><td>Detectados antes de compilar<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>No se trata de que un enfoque sea universalmente mejor que otro, sino de que <strong>Leptos reduce una clase concreta de problemas<\/strong> relacionados con la integraci\u00f3n entre frontend y backend.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Configuraci\u00f3n de Features: Un Binario, Dos Mundos<\/h2>\n\n\n\n<p>El <strong>Cargo.toml<\/strong> define dos features mutuamente exclusivas:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;features]\nssr = &#91;\n    \"dep:axum\",\n    \"dep:sqlx\",\n    \"dep:bcrypt\",\n    # ... dependencias del servidor\n]\nhydrate = &#91;\n    \"dep:wasm-bindgen\",\n    \"dep:console_error_panic_hook\",\n    # ... dependencias del cliente\n]<\/code><\/pre>\n\n\n\n<p>Al ejecutar <strong>cargo leptos watch<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El <strong>servidor<\/strong> se compila con <strong>&#8211;features ssr<\/strong><\/li>\n\n\n\n<li>El <strong>cliente<\/strong> con <strong>&#8211;features hydrate<\/strong><\/li>\n<\/ul>\n\n\n\n<p>Esto refuerza la separaci\u00f3n entre cliente y servidor sin duplicar c\u00f3digo ni modelos.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u00bfPodemos usar Rust en la Web?<\/h2>\n\n\n\n<p>Elegir Rust y Leptos no es solo una cuesti\u00f3n de rendimiento. Es un cambio de <strong>modelo mental<\/strong>. El compilador act\u00faa como una red de seguridad constante que ayuda a detectar inconsistencias entre capas antes de que lleguen a producci\u00f3n.<\/p>\n\n\n\n<p>La arquitectura <strong>Domain\u2013Server\u2013Frontend<\/strong> que se muestra aqu\u00ed no es la forma m\u00e1s r\u00e1pida de hacer un \u201cHola Mundo\u201d, pero s\u00ed una base s\u00f3lida para entender c\u00f3mo Rust puede simplificar el desarrollo full-stack al reducir una gran parte de los errores de integraci\u00f3n.<\/p>\n\n\n\n<p>Hice este corto tutorial para introducirlos en el tema, el objetivo no es cubrir todos los edge cases, sino mostrar <strong>por qu\u00e9 este enfoque resulta atractivo<\/strong> y c\u00f3mo pensar una aplicaci\u00f3n web full-stack desde Rust. Si te interesa revisar el c\u00f3digo de la aplicaci\u00f3n, puedes revisar el repositorio con el c\u00f3digo en <a href=\"https:\/\/github.com\/jure-ve\/user-leptos-app\">GitHub<\/a>.<\/p>\n\n\n\n<p>Sigamos codificando.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Si vienes del mundo de React, Next.js o Go, sabes que mantener la sincron\u00eda entre el frontend y el backend es una de las mayores fuentes de bugs. Rust, con el framework Leptos, propone una alternativa interesante: isomorfismo real. Un mismo lenguaje, tipos compartidos y una \u00fanica definici\u00f3n central de los datos y reglas del [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[10],"class_list":["post-147","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-rust"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/147","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=147"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/147\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=147"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=147"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=147"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}