← Volver al listado de tecnologías

Capitulo 14: Decisiones, Trade-offs y Proximos Pasos

Por: SiempreListo
rustwasmaxumleptos

Decisiones arquitectonicas

A lo largo del tutorial tomamos decisiones explicitas. Cada una tiene razones concretas y alternativas que descartamos.

1. Monorepo con Cargo Workspace

Decision: un unico repositorio con Cargo.toml en la raiz y tres miembros (backend, frontend, bbdd).

ProsContras
Un solo cargo build compila todoMayor tiempo de compilacion inicial
Dependencias compartidas via workspaceAcoplamiento entre crates
Refactoring atomicoCI mas complejo en proyectos grandes
Un solo Cargo.lockNo escala a equipos independientes

Alternativa descartada: repositorios separados. Para un proyecto de 856 lineas, la sobrecarga de sincronizar versiones entre repos no tiene sentido.

2. Arquitectura Hexagonal con traits

Decision: separar dominio, application e infrastructure usando traits como ports.

ProsContras
Dominio testeable sin I/OMas archivos y boilerplate
Adapters intercambiablesIndirection adicional
Reglas de negocio protegidasCurva de aprendizaje

Alternativa descartada: sin arquitectura, handlers que hablan directo con la BD. Funciona para prototipos pero no escala ni es testeable.

3. Async/await uniforme

Decision: Tokio en el backend, async via gloo-net en el frontend. Ambos lados usan async fn de forma natural.

La ventaja es consistencia: todo el codigo async se lee igual independientemente de donde se ejecute. La diferencia es que Tokio es multi-threaded y WASM es single-threaded, pero eso es transparente para el desarrollador.

4. Value Objects para type safety

Decision: CardId (wrapper de UUID) y Lane (enum) como tipos dedicados en vez de String.

El compilador rechaza move_card(lane, card_id) si invertimos los argumentos. Con String para ambos, ese bug seria silencioso. El costo es minimo: unas pocas lineas de impl y derive.

5. Raw SQL con SQLx vs ORM

Decision: queries SQL escritas a mano con verificacion en compilacion via sqlx::query!.

SQLx (elegido)Diesel/SeaORM
SQL explicito y visibleSQL generado, a veces opaco
Verificacion en compilacionMigraciones mas automatizadas
Sin DSL que aprenderAPI mas idiomatica en Rust
Control total sobre queriesRelaciones y joins mas faciles

Para 4 queries simples (CRUD), un ORM agrega complejidad sin beneficio.

6. CSR con Leptos vs SSR vs Yew

Decision: Leptos 0.7 en modo CSR (Client-Side Rendering).

FrameworkPor que si / por que no
Leptos CSR (elegido)Signals reactivos, API moderna, buen DX
Leptos SSRRequiere integracion con Axum, mas complejo
YewModelo de componentes tipo React, mas verboso
DioxusBueno pero ecosistema WASM menos maduro
SycamoreMenos mantenido activamente

CSR simplifica el deploy: el frontend es un bundle estatico de HTML + WASM + JS. No necesita servidor para renderizar.

7. Drag & Drop nativo vs libreria

Decision: HTML5 Drag & Drop API directa via web-sys.

Para tres columnas sin reordenamiento dentro de una columna, la API nativa es suficiente. Una libreria como dnd-kit (via JS interop) agregaria complejidad y dependencias de JavaScript.

8. Signals reactivos vs Redux-style

Decision: RwSignal y Resource de Leptos en vez de un store global.

Los signals son locales al componente y se propagan automaticamente. Para una app de este tamano, un store centralizado (tipo Redux) seria sobreingenieria. Si la app creciera, Leptos soporta context providers como escalamiento natural.

Trade-offs identificados

Estas son limitaciones conscientes del proyecto. No son bugs; son decisiones de alcance.

1. URL hardcodeada en frontend

const BASE_URL: &str = "http://localhost:22000/api";

El frontend asume que el backend corre en localhost:22000. En produccion esto se resolveria con una variable de entorno en tiempo de build o un archivo de configuracion.

2. Sin loading/error states en UI

Cuando el frontend hace un request HTTP, no muestra spinner ni mensaje de error. El Resource de Leptos soporta estados de loading, pero no los implementamos para mantener el tutorial enfocado.

3. Sin paginacion

fetch_all() trae todas las tarjetas de la BD. Con 10 tarjetas funciona perfecto. Con 10,000 seria un problema. La solucion es paginar en el backend con LIMIT/OFFSET o cursor-based pagination.

4. Sin autenticacion

La API es publica. Cualquiera puede crear, mover o eliminar tarjetas. Para multi-usuario se necesitaria JWT o session cookies, y asociar tarjetas a usuarios.

5. Timestamps como TEXT en SQLite

created_at TEXT NOT NULL DEFAULT (datetime('now')),

SQLite no tiene tipo TIMESTAMP nativo. Almacenamos como TEXT en formato ISO 8601. Funciona bien, pero las comparaciones de fechas en queries SQL son menos eficientes que con tipos nativos de PostgreSQL.

6. Sin CI/CD

No hay pipeline de integracion continua. Los tests se ejecutan manualmente con cargo test. En un proyecto real, GitHub Actions ejecutaria tests, clippy y build en cada PR.

Proximos pasos para mejorar el proyecto

1. Docker y Docker Compose

Empaquetar backend y frontend en contenedores separados con un docker-compose.yml que levante ambos servicios. El backend serviria la API y el frontend seria un servidor de archivos estaticos (nginx).

2. GitHub Actions CI

Pipeline minimo que ejecute en cada push:

3. Tests de integracion

Tests que levanten el servidor Axum con SQLite in-memory y ejecuten requests HTTP reales contra la API. Esto cubre el flujo completo sin depender de infraestructura externa.

4. Loading states en UI

Usar los estados de Resource de Leptos para mostrar un spinner mientras se carga y un mensaje de error si falla. Esto mejora la UX significativamente.

5. Configuracion externalizada

Reemplazar const BASE_URL con una variable de entorno leida en tiempo de build con env!() o un archivo .env procesado por Trunk.

6. SSR con Leptos

Migrar de CSR a SSR integrando Leptos directamente con Axum. Esto mejora el SEO y el tiempo de primera carga, a costa de mayor complejidad en el deploy.

7. Autenticacion con JWT

Agregar middleware de autenticacion en Axum que valide tokens JWT. El frontend enviaria el token en headers Authorization: Bearer. Requiere un flujo de login/registro.

Metricas del proyecto

El proyecto completo es intencionalmente compacto:

MetricaValor
Lineas de codigo856
Archivos Rust42
Maximo por archivo< 150 lineas
Crates en workspace3 (backend, frontend, bbdd)
Endpoints API4 (GET, POST, PATCH, DELETE)
Componentes UI4 (Board, Lane, CardItem, CreateCardForm)
Tests6 unitarios

Estas metricas demuestran que Rust fullstack no requiere miles de lineas de boilerplate. Con tipos fuertes, traits y un buen diseno, poco codigo hace mucho.

Conclusion

Rust fullstack con WASM es viable y productivo. Lo que construimos en este tutorial:

Rust no es solo para sistemas operativos o herramientas CLI. Con el ecosistema actual (Axum, Leptos, SQLx, Trunk), es una opcion real para aplicaciones web donde la seguridad de tipos, el rendimiento y la confiabilidad importan.

El mayor beneficio no es la velocidad del binario; es la confianza que da el compilador. Si compila, la mayoria de los bugs de tipos, nulls y concurrencia simplemente no existen.


← Anterior: Testing | Volver al indice