Capitulo 4: Setup del Cargo Workspace

Por: SiempreListo
rustwasmaxumleptos

Que es un Cargo Workspace

Un Cargo Workspace permite gestionar multiples crates (proyectos) en un solo repositorio. Comparten un unico Cargo.lock y directorio target/, lo que garantiza versiones consistentes y compilaciones mas rapidas por cache compartido.

Para nuestro Kanban, el workspace agrupa backend y frontend en un mismo repositorio pero como binarios independientes.

Cargo.toml raiz

El archivo raiz solo define los miembros del workspace:

[workspace]
resolver = "2"
members = [
    "apps/app-backend",
    "apps/app-frontend",
]
  • resolver = "2": usa el resolver de dependencias moderno de Cargo (requerido desde edition 2021)
  • members: lista de crates que componen el workspace

No hay dependencias aqui. Cada crate define las suyas.

Crear la estructura

# Crear el directorio raiz
mkdir rust-poc && cd rust-poc

# Crear los proyectos
mkdir -p apps
cargo new apps/app-backend
cargo new apps/app-frontend

# Crear directorio de migraciones
mkdir -p apps/app-bbdd/migrations

El directorio app-bbdd no es un crate de Rust: es simplemente una carpeta con archivos SQL y la base de datos SQLite.

Dependencias del Backend

apps/app-backend/Cargo.toml

[package]
name = "app-backend"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
tower-http = { version = "0.6", features = ["cors"] }
thiserror = "2"
anyhow = "1"
uuid = { version = "1", features = ["v4", "serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
dotenvy = "0.15"
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
mockall = "0.13"
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"

Explicacion de cada dependencia

Framework y runtime:

CrateVersionPara que sirve
axum0.8Framework HTTP. Define rutas, extractors y handlers
tokio1 (full)Runtime async. full habilita IO, timers, macros, multithreading
tower-http0.6 (cors)Middleware HTTP. Usamos CorsLayer::permissive() para permitir requests del frontend WASM

Serializacion:

CrateVersionPara que sirve
serde1 (derive)Serializar/deserializar structs. derive habilita #[derive(Serialize, Deserialize)]
serde_json1Convertir entre structs Rust y JSON

Base de datos:

CrateVersionPara que sirve
sqlx0.8Queries SQL async. runtime-tokio integra con nuestro runtime, sqlite habilita el driver SQLite

Manejo de errores:

CrateVersionPara que sirve
thiserror2Macro #[derive(Error)] para crear enums de error con mensajes. Usado en DomainError
anyhow1Error generico para main() y situaciones donde no necesitas un tipo especifico

Utilidades:

CrateVersionPara que sirve
uuid1 (v4, serde)Genera UUIDs v4 para CardId. serde permite serializar/deserializar UUIDs
chrono0.4 (serde)Manejo de fechas (DateTime<Utc>). serde para serializar timestamps
tracing0.1Logging estructurado. tracing::info!("Servidor en http://localhost:{port}")
tracing-subscriber0.3Configura la salida de logs (formato, nivel)
dotenvy0.15Carga variables de entorno desde .env

Dev dependencies (solo para tests):

CrateVersionPara que sirve
mockall0.13Genera mocks automaticos desde traits con #[cfg_attr(test, mockall::automock)]
tower0.5 (util)Utilidades para testear servicios Tower/Axum
http-body-util0.1Helpers para leer bodies HTTP en tests

Dependencias del Frontend

apps/app-frontend/Cargo.toml

[package]
name = "app-frontend"
version = "0.1.0"
edition = "2021"

[dependencies]
leptos = { version = "0.7", features = ["csr"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde", "js"] }
gloo-net = { version = "0.6", features = ["http"] }
web-sys = { version = "0.3", features = [
    "DragEvent",
    "DataTransfer",
    "HtmlElement",
    "Element",
] }
wasm-bindgen = "0.2"

Explicacion de cada dependencia

CrateVersionPara que sirve
leptos0.7 (csr)Framework UI reactivo. csr = Client-Side Rendering, compila a WASM sin servidor
serde1 (derive)Deserializar las respuestas JSON del backend a structs Rust
serde_json1Construir bodies JSON para enviar al backend
uuid1 (v4, serde, js)UUIDs en el frontend. js usa crypto.getRandomValues() del navegador en vez de /dev/urandom
gloo-net0.6 (http)Cliente HTTP para WASM. Wrapper sobre la Fetch API del navegador
web-sys0.3Bindings a las Web APIs del navegador. Cada feature habilita una API especifica
wasm-bindgen0.2Puente entre Rust/WASM y JavaScript. Requerido por web-sys y gloo

Features de web-sys detalladas

web-sys es modular: solo incluyes las APIs que necesitas. Para el drag & drop:

  • DragEvent: eventos dragstart, dragover, drop
  • DataTransfer: getData()/setData() para transferir el ID de la tarjeta
  • HtmlElement y Element: manipulacion basica del DOM

Archivo .env

DATABASE_URL=sqlite:apps/app-bbdd/kanban.db?mode=rwc
PORT=22000
  • DATABASE_URL: ruta relativa al archivo SQLite. ?mode=rwc significa Read-Write-Create (crea el archivo si no existe)
  • PORT: puerto donde escucha Axum. Elegimos 22000 para no colisionar con puertos comunes

Migracion SQL

apps/app-bbdd/migrations/001_create_cards.sql

CREATE TABLE IF NOT EXISTS cards (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    lane TEXT NOT NULL DEFAULT 'todo',
    position INTEGER NOT NULL DEFAULT 0,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

La migracion se ejecuta al iniciar el backend con include_str!:

sqlx::query(include_str!("../../app-bbdd/migrations/001_create_cards.sql"))
    .execute(&pool)
    .await?;

include_str! embebe el contenido del archivo SQL en el binario en tiempo de compilacion. CREATE TABLE IF NOT EXISTS es idempotente: se puede ejecutar multiples veces sin error.

Compilacion WASM con Trunk

Para compilar el frontend a WASM necesitas Trunk, un bundler para aplicaciones Rust/WASM:

# Instalar Trunk
cargo install trunk

# Agregar el target WASM
rustup target add wasm32-unknown-unknown

Trunk busca un archivo index.html en la raiz del crate frontend:

<!-- apps/app-frontend/index.html -->
<!DOCTYPE html>
<html>
<head>
    <link data-trunk rel="css" href="style.css" />
</head>
<body></body>
</html>

Trunk automaticamente detecta que es un proyecto Rust, compila a WASM, genera los archivos JS de pegamento (glue code), e inyecta los scripts en el HTML.

Verificar que todo compila

# Compilar todo el workspace (backend nativo)
cargo build --workspace

# Compilar solo el frontend a WASM
cd apps/app-frontend && trunk build

# Ejecutar el backend
cargo run -p app-backend

# Servir el frontend con hot-reload
cd apps/app-frontend && trunk serve

Si cargo build --workspace compila sin errores, la estructura esta correcta. El backend compila a binario nativo y el frontend se compila por separado a WASM con Trunk.

Resumen

La configuracion del workspace es minimalista:

  • 1 archivo Cargo.toml raiz con la lista de miembros
  • 2 crates: backend (binario nativo) y frontend (compila a WASM)
  • 1 directorio app-bbdd con migraciones SQL
  • 1 archivo .env con configuracion

El Cargo Workspace permite que ambos crates compartan Cargo.lock (versiones consistentes) y target/ (cache de compilacion), mientras cada uno define sus propias dependencias especificas para su plataforma.


← Anterior: Arquitectura Hexagonal | Siguiente: Dominio - Entidades y Value Objects →