ISW · Taller Node.js
🛠️ Taller · complemento de la guía de arquitectura por capas

Arma las capas de EventoUAB desde cero, en Node.js

En la guía anterior viste el diagrama y el código ya hecho. Acá lo construyes tú, desde la carpeta vacía: qué comandos escribir, qué archivos crear, en qué carpeta va cada uno y cómo comprobar en cada paso que todo funciona. Al terminar tendrás un proyecto real con el dominio protegido, pruebas que corren sin base de datos y un adaptador de SQLite.

👁 — vistas · Ing. Roy Carrasco, Facultad de Ingeniería de Sistemas · UAB

Si todavía no viste la guía de arquitectura por capas, léela primero: acá no se vuelve a explicar el porqué, solo el cómo.

0. Qué necesitas antes de empezar

Tres cosas instaladas y una terminal abierta.

HerramientaPara quéCómo comprobarla
Node.js 22 o superior (LTS)Ejecutar JavaScript fuera del navegador. Trae npm y un ejecutor de pruebas incluidos.Escribe node -v en la terminal.
Un editor de códigoCrear los archivos. Sirve Visual Studio Code.Que abra una carpeta y muestre su contenido.
Una terminalEscribir comandos. En Windows, PowerShell; en macOS o Linux, Terminal.Que responda a node -v con un número de versión.
Terminal — comprobar la instalación
node -v
npm -v
Nada de frameworks

Este proyecto no usa Express ni ninguna otra librería web, a propósito: con una librería de más, la lección se mezcla con otra cosa. La única dependencia externa será la de SQLite, y solo en el último paso.

1. Crear la carpeta y el proyecto

Un proyecto de Node es una carpeta con un archivo package.json que describe qué es y qué comandos tiene. Lo creamos desde la terminal.

Terminal — crear la carpeta y el package.json
mkdir eventouab
cd eventouab
npm init -y

El comando npm init -y crea el archivo con valores por defecto. Ábrelo en tu editor y déjalo así, con dos cambios importantes: "type": "module" (para poder usar import y export) y los dos scripts.

package.json
{
  "name": "eventouab",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "node --test test/*.test.js",
    "start": "node src/main.js"
  },
  "dependencies": {
    "better-sqlite3": "^13.0.3"
  }
}
Por qué "type": "module"

Sin esa línea, Node trata los archivos .js como el formato antiguo y falla apenas ve un import. Es el error más común del taller (mira la tabla de errores al final).

2. Crear las carpetas: una por capa

La estructura de carpetas es la arquitectura hecha visible: quien abre el proyecto debe ver las capas sin leer ningún archivo.

Terminal — crear las carpetas (Windows, PowerShell)
mkdir src\dominio, src\infraestructura, test
Terminal — crear las carpetas (macOS o Linux)
mkdir -p src/dominio src/infraestructura test
Cómo debe quedar el proyecto al terminar
eventouab/
├── package.json
├── src/
│   ├── dominio/                 ← el centro: entidades, reglas y puertos
│   │   ├── reserva.js
│   │   ├── puertos.js
│   │   └── reservarEspacio.js
│   ├── infraestructura/         ← los detalles que pueden cambiar
│   │   └── repoSqlite.js
│   └── main.js                  ← arma las piezas y las pone a andar
└── test/
    ├── repoEnMemoria.js         ← adaptador de pruebas
    ├── reservarEspacio.test.js
    └── arquitectura.test.js
CarpetaQué va adentroQué puede importar
src/dominioEntidades, reglas del negocio, casos de uso y puertos.Solo archivos de su propia carpeta.
src/infraestructuraCódigo que habla con la base de datos u otros servicios.El dominio y las librerías externas.
testPruebas y adaptadores que solo sirven para probar.Cualquier cosa del proyecto.
src/main.jsEl punto de entrada: elige qué adaptador usar y arranca.Todo. Es el único que conoce todas las piezas.
main.js no pertenece a ninguna capa

Es el lugar donde se decide, por ejemplo, «usar SQLite» o «usar memoria». Esa decisión no corresponde al dominio ni a la infraestructura: por eso vive afuera de las dos carpetas.

3. El dominio: tres archivos que no importan nada de afuera

Crea cada archivo dentro de src/dominio y copia su contenido. Para cada uno, fíjate en qué import tiene (o no).

3.1 La entidad. src/dominio/reserva.js no tiene ningún import. Además de guardar datos, defiende una regla: el fin debe ser posterior al inicio.

src/dominio/reserva.js
export class Reserva {
  constructor(clubId, espacioId, inicio, fin) {
    if (fin <= inicio) throw new Error("El fin debe ser posterior al inicio");
    this.clubId = clubId;
    this.espacioId = espacioId;
    this.inicio = inicio;
    this.fin = fin;
  }
}

3.2 El puerto. src/dominio/puertos.js declara lo que el dominio necesita de afuera, sin decir cómo se hace. JavaScript no tiene interfaces, así que usamos una clase base cuyos métodos fallan si nadie los implementa.

src/dominio/puertos.js
export class RepositorioDeReservas {
  hayConflicto(espacioId, inicio, fin) { throw new Error("no implementado"); }
  guardar(reserva) { throw new Error("no implementado"); }
}

3.3 El caso de uso. src/dominio/reservarEspacio.js importa solo a reserva.js, que está en la misma carpeta. El repositorio llega por el constructor: la clase no sabe cuál es.

src/dominio/reservarEspacio.js
import { Reserva } from "./reserva.js";

export class ReservarEspacio {
  constructor(repo) { this.repo = repo; }

  ejecutar(clubId, espacioId, inicio, fin) {
    const reserva = new Reserva(clubId, espacioId, inicio, fin);
    if (this.repo.hayConflicto(espacioId, inicio, fin)) {
      throw new Error("Espacio ocupado en ese horario");
    }
    this.repo.guardar(reserva);
    return reserva;
  }
}
Detalle de Node: la extensión es obligatoria

En módulos ES, los imports de archivos propios llevan siempre la extensión: "./reserva.js", no "./reserva". Olvidarla produce el error ERR_MODULE_NOT_FOUND.

4. Probar las reglas sin base de datos

Todavía no existe ninguna base de datos y ya puedes probar la regla de solapamiento. Para eso hace falta un adaptador de pruebas: un repositorio que guarda en un arreglo.

4.1 El repositorio en memoria. Va en test/ porque solo sirve para probar; el programa real nunca lo usará.

test/repoEnMemoria.js
import { RepositorioDeReservas } from "../src/dominio/puertos.js";

export class RepoEnMemoria extends RepositorioDeReservas {
  constructor() { super(); this.reservas = []; }

  hayConflicto(espacioId, inicio, fin) {
    return this.reservas.some(
      (r) => r.espacioId === espacioId && r.inicio < fin && r.fin > inicio);
  }

  guardar(reserva) { this.reservas.push(reserva); }
}

4.2 Las pruebas del caso de uso. Usan el ejecutor que ya trae Node (node:test), sin instalar nada.

test/reservarEspacio.test.js
import { test } from "node:test";
import assert from "node:assert";
import { ReservarEspacio } from "../src/dominio/reservarEspacio.js";
import { RepoEnMemoria } from "./repoEnMemoria.js";

const hora = (h) => new Date(`2026-10-10T${h}:00`);

test("reserva un espacio libre", () => {
  const caso = new ReservarEspacio(new RepoEnMemoria());
  const reserva = caso.ejecutar(1, 7, hora("14"), hora("16"));
  assert.strictEqual(reserva.espacioId, 7);
});

test("no permite reservar un horario que se solapa", () => {
  const caso = new ReservarEspacio(new RepoEnMemoria());
  caso.ejecutar(1, 7, hora("14"), hora("16"));
  assert.throws(
    () => caso.ejecutar(2, 7, hora("15"), hora("17")),
    /Espacio ocupado/);
});

test("permite el mismo horario en otro espacio", () => {
  const caso = new ReservarEspacio(new RepoEnMemoria());
  caso.ejecutar(1, 7, hora("14"), hora("16"));
  assert.doesNotThrow(() => caso.ejecutar(2, 8, hora("14"), hora("16")));
});

4.3 Ejecuta las pruebas.

Terminal
npm test
Salida esperada (los tiempos varían)
✔ reserva un espacio libre
✔ no permite reservar un horario que se solapa
✔ permite el mismo horario en otro espacio
ℹ tests 3
ℹ pass 3
ℹ fail 0

Los tres pasan en milisegundos, sin base de datos, sin red y sin librerías. Esa es la ganancia de que el dominio dependa de un puerto y no de SQLite.

5. Un test que vigila la regla de dependencia

Una regla de arquitectura que solo está escrita en un documento se rompe sola con el tiempo. Mejor convertirla en una prueba: este test lee los archivos del dominio y falla si alguno importa algo de afuera.

test/arquitectura.test.js
import { test } from "node:test";
import assert from "node:assert";
import { readdirSync, readFileSync } from "node:fs";

test("el dominio no importa nada de afuera", () => {
  const carpeta = "src/dominio";
  for (const archivo of readdirSync(carpeta)) {
    const codigo = readFileSync(`${carpeta}/${archivo}`, "utf8");
    const imports = [...codigo.matchAll(/from\s+"([^"]+)"/g)].map((m) => m[1]);
    for (const origen of imports) {
      assert.ok(origen.startsWith("./"),
        `${archivo} importa "${origen}", pero el dominio solo puede importar dentro del dominio`);
    }
  }
});
Terminal
npm test

Ahora ejecútalo con una violación. Agrega esta línea al principio de src/dominio/reserva.js, corre npm test y observa cómo falla:

Línea que rompe la regla (bórrala después)
import Database from "better-sqlite3";
Salida esperada de la prueba que falla
✖ el dominio no importa nada de afuera
  AssertionError: reserva.js importa "better-sqlite3", pero el dominio
  solo puede importar dentro del dominio

Borra la línea y vuelve a correr npm test: deben pasar las cuatro pruebas.

6. El adaptador de SQLite y el punto de entrada

Recién ahora aparece una base de datos real, y lo hace en la capa de afuera, sin tocar una sola línea del dominio.

6.1 Instala la librería. Es la única dependencia externa del proyecto.

Terminal
npm install better-sqlite3

Esto agrega la carpeta node_modules y una sección dependencies en package.json. Crea también un archivo .gitignore en la raíz para no subir a Git lo que no corresponde:

.gitignore
node_modules/
*.db

6.2 El adaptador. src/infraestructura/repoSqlite.js importa el puerto del dominio: la flecha apunta hacia el centro.

src/infraestructura/repoSqlite.js
import { RepositorioDeReservas } from "../dominio/puertos.js";

export class RepoSqlite extends RepositorioDeReservas {
  constructor(db) { super(); this.db = db; }

  hayConflicto(espacioId, inicio, fin) {
    const fila = this.db
      .prepare("SELECT 1 FROM reserva WHERE espacio_id = ? AND inicio < ? AND fin > ?")
      .get(espacioId, fin.toISOString(), inicio.toISOString());
    return fila !== undefined;
  }

  guardar(r) {
    this.db
      .prepare("INSERT INTO reserva VALUES (?, ?, ?, ?)")
      .run(r.clubId, r.espacioId, r.inicio.toISOString(), r.fin.toISOString());
  }
}

6.3 El punto de entrada. src/main.js es el único archivo que conoce las dos capas: crea la base, elige el adaptador y se lo entrega al caso de uso.

src/main.js
import Database from "better-sqlite3";
import { ReservarEspacio } from "./dominio/reservarEspacio.js";
import { RepoSqlite } from "./infraestructura/repoSqlite.js";

const db = new Database("eventouab.db");
db.exec(`CREATE TABLE IF NOT EXISTS reserva (
  club_id INTEGER, espacio_id INTEGER, inicio TEXT, fin TEXT)`);

const caso = new ReservarEspacio(new RepoSqlite(db));
const hora = (h) => new Date(`2026-10-10T${h}:00`);

caso.ejecutar(1, 7, hora("14"), hora("16"));
console.log("Reserva guardada.");

try {
  caso.ejecutar(2, 7, hora("15"), hora("17"));
} catch (e) {
  console.log("Rechazada:", e.message);
}

6.4 Ejecuta el programa.

Terminal
npm start
Salida esperada la primera vez
Reserva guardada.
Rechazada: Espacio ocupado en ese horario

Se crea el archivo eventouab.db en la carpeta. Si ejecutas npm start otra vez sin borrarlo, el programa termina con un error «Espacio ocupado en ese horario»: es correcto, porque la primera reserva ya quedó guardada y el horario está ocupado. Los datos persisten. Para empezar de cero, borra el archivo eventouab.db.

Lo que acabas de demostrar

Las mismas reglas corrieron con un arreglo en memoria (en las pruebas) y con SQLite (en el programa), sin cambiar ninguna línea de src/dominio. Si mañana Bienestar pide PostgreSQL, escribes repoPostgres.js y cambias una línea de main.js.

Errores frecuentes y cómo resolverlos

Lo que vesCausa probableSolución
Cannot use import statement outside a moduleFalta "type": "module" en package.json.Agrégalo y vuelve a ejecutar.
ERR_MODULE_NOT_FOUND con una ruta propiaUn import sin extensión o con la ruta mal escrita.Escribe "./reserva.js" y revisa mayúsculas y carpetas.
Cannot find package 'better-sqlite3'No ejecutaste la instalación, o estás en otra carpeta.Ve a la carpeta del proyecto y corre npm install better-sqlite3.
no implementadoTu adaptador no define uno de los métodos del puerto, o lo escribiste con otro nombre.Revisa que se llamen exactamente hayConflicto y guardar.
npm test no encuentra pruebasLos archivos de prueba no terminan en .test.js o no están en test/.Renómbralos según la estructura del paso 2.

Actividades de repaso

Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.

0 / 0
actividades correctas en toda la guía
Clasificación

¿En qué carpeta va cada archivo?

reservarEspacio.js, el caso de uso.
repoSqlite.js, que escribe el INSERT.
repoEnMemoria.js, que guarda en un arreglo.
puertos.js, con RepositorioDeReservas.
arquitectura.test.js, que vigila los imports.
reserva.js, con la regla del fin posterior al inicio.

Selección múltiple

Al ejecutar npm test aparece «Cannot use import statement outside a module». ¿Qué falta?

Selección múltiple

¿Por qué repoEnMemoria.js está en test/ y no en src/?

🎯 Reto: agrega una regla nueva, primero con la prueba

EventoUAB ahora pide que ninguna reserva dure más de cuatro horas. Sin tocar la infraestructura, escribe: 1) la prueba que falla (una reserva de cinco horas debe lanzar un error), 2) en qué archivo del dominio pondrás la regla y por qué ahí, 3) el cambio mínimo de código que hace pasar la prueba, 4) cómo confirmas con npm test que las pruebas anteriores siguen pasando y que el test de arquitectura no se rompió.

Análisis abierto

Una regla nueva del dominio, escrita primero como prueba.