← Volver al listado de tecnologías

Capítulo 6: Configuración del Proyecto TypeScript

Por: SiempreListo
event-sourcingtypescriptbunsetuparquitectura

Capítulo 6: Configuración del Proyecto TypeScript

“Una buena estructura de proyecto es la base de un sistema mantenible”

Inicialización del Proyecto

# Crear directorio
mkdir orderflow-event-sourcing && cd orderflow-event-sourcing

# Inicializar con Bun
bun init -y

# Instalar dependencias principales
bun add zod uuid drizzle-orm postgres hono @hono/node-server

# Dependencias de desarrollo
bun add -d typescript @types/node @types/uuid vitest drizzle-kit

Estructura del Proyecto

El proyecto sigue una arquitectura hexagonal (también llamada ports and adapters). Esta arquitectura separa claramente:

orderflow-event-sourcing/
├── src/
│   ├── domain/                    # Núcleo del negocio (sin dependencias externas)
│   │   ├── events/                # Eventos de dominio
│   │   │   ├── base.ts
│   │   │   └── order-events.ts
│   │   ├── aggregates/            # Agregados
│   │   │   └── order/
│   │   │       ├── order.ts
│   │   │       └── order.test.ts
│   │   └── value-objects/         # Value Objects
│   │       ├── address.ts
│   │       └── money.ts
│   │
│   ├── application/               # Casos de uso
│   │   ├── commands/              # Command handlers
│   │   │   ├── create-order.ts
│   │   │   └── confirm-order.ts
│   │   ├── queries/               # Query handlers
│   │   │   └── get-order.ts
│   │   └── projections/           # Proyecciones
│   │       └── orders-projection.ts
│   │
│   ├── infrastructure/            # Implementaciones técnicas
│   │   ├── event-store/           # Event Store
│   │   │   ├── types.ts
│   │   │   ├── postgres-event-store.ts
│   │   │   └── in-memory-event-store.ts
│   │   ├── repositories/          # Repositorios
│   │   │   └── order-repository.ts
│   │   └── database/              # Configuración DB
│   │       ├── schema.ts
│   │       └── connection.ts
│   │
│   └── api/                       # Capa de presentación
│       ├── routes/
│       │   └── orders.ts
│       └── server.ts

├── tests/                         # Tests de integración
├── drizzle/                       # Migraciones
├── docker-compose.yml
├── tsconfig.json
└── package.json

Configuración TypeScript

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "resolveJsonModule": true,
    "paths": {
      "@domain/*": ["./src/domain/*"],
      "@application/*": ["./src/application/*"],
      "@infrastructure/*": ["./src/infrastructure/*"],
      "@api/*": ["./src/api/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Docker Compose

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: orderflow
      POSTGRES_PASSWORD: orderflow
      POSTGRES_DB: orderflow
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Esquema de Base de Datos

// src/infrastructure/database/schema.ts
import { pgTable, serial, varchar, jsonb, timestamp, integer, bigint, unique, index, decimal } from 'drizzle-orm/pg-core';

// Event Store
export const events = pgTable('events', {
  globalPosition: bigint('global_position', { mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(),
  streamId: varchar('stream_id', { length: 255 }).notNull(),
  streamPosition: integer('stream_position').notNull(),
  eventType: varchar('event_type', { length: 255 }).notNull(),
  data: jsonb('data').notNull(),
  metadata: jsonb('metadata').default({}),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow()
}, (table) => ({
  streamPositionUnique: unique().on(table.streamId, table.streamPosition),
  streamIdIndex: index('idx_stream_id').on(table.streamId),
  eventTypeIndex: index('idx_event_type').on(table.eventType)
}));

// Read Model: Orders View
export const ordersView = pgTable('orders_view', {
  id: varchar('id', { length: 255 }).primaryKey(),
  customerId: varchar('customer_id', { length: 255 }).notNull(),
  status: varchar('status', { length: 50 }).notNull(),
  itemCount: integer('item_count').default(0),
  total: decimal('total', { precision: 10, scale: 2 }).default('0'),
  createdAt: timestamp('created_at', { withTimezone: true }),
  updatedAt: timestamp('updated_at', { withTimezone: true })
}, (table) => ({
  customerIdIndex: index('idx_orders_customer').on(table.customerId),
  statusIndex: index('idx_orders_status').on(table.status)
}));

// Read Model: Order Items View
export const orderItemsView = pgTable('order_items_view', {
  id: serial('id').primaryKey(),
  orderId: varchar('order_id', { length: 255 }).notNull(),
  productId: varchar('product_id', { length: 255 }).notNull(),
  productName: varchar('product_name', { length: 255 }).notNull(),
  quantity: integer('quantity').notNull(),
  unitPrice: decimal('unit_price', { precision: 10, scale: 2 }).notNull()
}, (table) => ({
  orderIdIndex: index('idx_items_order').on(table.orderId)
}));

// Snapshots
export const snapshots = pgTable('snapshots', {
  streamId: varchar('stream_id', { length: 255 }).primaryKey(),
  version: integer('version').notNull(),
  data: jsonb('data').notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow()
});

// Projection checkpoints
export const checkpoints = pgTable('checkpoints', {
  projectionName: varchar('projection_name', { length: 255 }).primaryKey(),
  position: bigint('position', { mode: 'bigint' }).notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow()
});

Conexión a Base de Datos

// src/infrastructure/database/connection.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const connectionString = process.env.DATABASE_URL
  ?? 'postgres://orderflow:orderflow@localhost:5432/orderflow';

const client = postgres(connectionString);
export const db = drizzle(client, { schema });

export type Database = typeof db;

Drizzle Config

// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/infrastructure/database/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL
      ?? 'postgres://orderflow:orderflow@localhost:5432/orderflow'
  }
} satisfies Config;

Package.json Scripts

{
  "name": "orderflow-event-sourcing",
  "type": "module",
  "scripts": {
    "dev": "bun run --watch src/api/server.ts",
    "build": "bun build src/api/server.ts --outdir dist",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio"
  }
}

Configuración Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts']
    }
  },
  resolve: {
    alias: {
      '@domain': path.resolve(__dirname, './src/domain'),
      '@application': path.resolve(__dirname, './src/application'),
      '@infrastructure': path.resolve(__dirname, './src/infrastructure'),
      '@api': path.resolve(__dirname, './src/api')
    }
  }
});

Variables de Entorno

# .env
DATABASE_URL=postgres://orderflow:orderflow@localhost:5432/orderflow
REDIS_URL=redis://localhost:6379
PORT=3000
NODE_ENV=development

Servidor API Base

// src/api/server.ts
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { serve } from '@hono/node-server';

const app = new Hono();

app.use('*', logger());

app.get('/health', (c) => c.json({ status: 'ok' }));

const port = Number(process.env.PORT) || 3000;

console.log(`Server starting on port ${port}`);

serve({ fetch: app.fetch, port });

export default app;

Ejecutar el Proyecto

# 1. Iniciar servicios
docker-compose up -d

# 2. Generar migraciones
bun run db:generate

# 3. Aplicar migraciones
bun run db:migrate

# 4. Iniciar servidor
bun run dev

# 5. Verificar health
curl http://localhost:3000/health

Resumen

Glosario

Arquitectura Hexagonal

Definicion: Patron arquitectonico que separa el nucleo de negocio (dominio) de los detalles tecnicos (infraestructura) mediante puertos y adaptadores.

Por que es importante: Permite cambiar la base de datos, el framework HTTP, o cualquier detalle tecnico sin modificar las reglas de negocio. Facilita testing y mantenibilidad.

Ejemplo practico: El agregado Order no sabe si se guarda en PostgreSQL o MongoDB. El repositorio (infraestructura) implementa una interfaz (puerto) que el dominio define.


Domain Layer (Capa de Dominio)

Definicion: Capa que contiene las reglas de negocio puras: agregados, eventos, value objects. No tiene dependencias externas.

Por que es importante: Es el corazon de la aplicacion. Si las reglas de negocio estan aisladas, son faciles de probar y no cambian cuando cambia la tecnologia.

Ejemplo practico: La carpeta src/domain/ contiene Order, OrderCreated, Money, Address. Ninguno de estos archivos importa drizzle, hono, o cualquier libreria externa.


Application Layer (Capa de Aplicacion)

Definicion: Capa que orquesta el dominio para cumplir casos de uso. Contiene command handlers, query handlers, y proyecciones.

Por que es importante: Coordina el flujo: recibe comando, carga agregado, ejecuta logica, guarda eventos, actualiza proyecciones.

Ejemplo practico: CreateOrderHandler recibe CreateOrderCommand, llama Order.create(), usa el repositorio para guardar los eventos, y retorna el resultado.


Infrastructure Layer (Capa de Infraestructura)

Definicion: Capa que contiene implementaciones tecnicas: base de datos, APIs externas, sistemas de mensajeria.

Por que es importante: Aisla los detalles tecnicos. Si decides cambiar de PostgreSQL a MongoDB, solo modificas esta capa.

Ejemplo practico: PostgresEventStore implementa la interfaz EventStore usando Drizzle y PostgreSQL. El dominio solo conoce la interfaz, no la implementacion.


Value Object

Definicion: Objeto inmutable que se define por sus atributos, no por una identidad. Dos value objects con los mismos valores son iguales.

Por que es importante: Encapsulan conceptos del dominio con sus validaciones. Money(100, "USD") es mas expresivo y seguro que dos variables separadas.

Ejemplo practico: Address { street, city, zipCode, country } es un value object. Dos direcciones con los mismos valores son la misma direccion. No tienen ID propio.


Path Aliases (@domain, @infrastructure)

Definicion: Configuracion de TypeScript que permite importar modulos usando alias en lugar de rutas relativas.

Por que es importante: Mejora la legibilidad y facilita refactoring. @domain/events/base es mas claro que ../../../domain/events/base.

Ejemplo practico: En tsconfig.json defines "@domain/*": ["./src/domain/*"]. Luego importas import { Order } from '@domain/aggregates/order'.


Migracion de Base de Datos

Definicion: Archivo que describe cambios al esquema de la base de datos (crear tablas, agregar columnas, etc.).

Por que es importante: Permite versionar el esquema junto con el codigo. Todos los desarrolladores y ambientes tienen la misma estructura.

Ejemplo practico: drizzle-kit generate crea un archivo SQL con CREATE TABLE events (...). drizzle-kit migrate ejecuta las migraciones pendientes.


Drizzle ORM

Definicion: ORM (Object-Relational Mapping) ligero para TypeScript que proporciona type-safety completo y genera SQL optimizado.

Por que es importante: Permite escribir queries en TypeScript con autocompletado y validacion de tipos, reduciendo errores en tiempo de ejecucion.

Ejemplo practico: db.select().from(events).where(eq(events.streamId, "order-123")) genera SELECT * FROM events WHERE stream_id = 'order-123' con tipos correctos.


Hono

Definicion: Framework web ultraligero para TypeScript, similar a Express pero optimizado para edge computing y con mejor tipado.

Por que es importante: Simple, rapido, y funciona en multiples runtimes (Node, Bun, Cloudflare Workers). Ideal para APIs REST.

Ejemplo practico: app.post('/orders', (c) => { ... }) define un endpoint. Hono maneja routing, middleware, y serialization de forma elegante.


← Capítulo 5: Proyecciones | Capítulo 7: Modelando Eventos →