Capitulo 14: Decisiones, Trade-offs y Proximos Pasos
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).
| Pros | Contras |
|---|---|
Un solo cargo build compila todo | Mayor tiempo de compilacion inicial |
| Dependencias compartidas via workspace | Acoplamiento entre crates |
| Refactoring atomico | CI mas complejo en proyectos grandes |
Un solo Cargo.lock | No 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.
| Pros | Contras |
|---|---|
| Dominio testeable sin I/O | Mas archivos y boilerplate |
| Adapters intercambiables | Indirection adicional |
| Reglas de negocio protegidas | Curva 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 visible | SQL generado, a veces opaco |
| Verificacion en compilacion | Migraciones mas automatizadas |
| Sin DSL que aprender | API mas idiomatica en Rust |
| Control total sobre queries | Relaciones 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).
| Framework | Por que si / por que no |
|---|---|
| Leptos CSR (elegido) | Signals reactivos, API moderna, buen DX |
| Leptos SSR | Requiere integracion con Axum, mas complejo |
| Yew | Modelo de componentes tipo React, mas verboso |
| Dioxus | Bueno pero ecosistema WASM menos maduro |
| Sycamore | Menos 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:
cargo fmt --checkcargo clippy -- -D warningscargo testtrunk build(verificar que el frontend compila a WASM)
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:
| Metrica | Valor |
|---|---|
| Lineas de codigo | 856 |
| Archivos Rust | 42 |
| Maximo por archivo | < 150 lineas |
| Crates en workspace | 3 (backend, frontend, bbdd) |
| Endpoints API | 4 (GET, POST, PATCH, DELETE) |
| Componentes UI | 4 (Board, Lane, CardItem, CreateCardForm) |
| Tests | 6 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:
- Backend robusto con Axum, arquitectura hexagonal y SQLite
- Frontend reactivo compilado a WebAssembly con Leptos
- Dominio compartido con tipos seguros de punta a punta
- Testing integrado en el lenguaje sin frameworks pesados
- 856 lineas para una aplicacion funcional con drag & drop
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.