Capítulo 6: Configuración del Proyecto TypeScript
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:
- Domain: Las reglas de negocio puras, sin dependencias externas
- Application: Los casos de uso que orquestan el dominio
- Infrastructure: Implementaciones técnicas (bases de datos, APIs externas)
- API: La capa de presentación (HTTP, CLI, etc.)
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
- Proyecto estructurado con arquitectura hexagonal
- Bun como runtime y package manager
- Drizzle ORM para base de datos
- Hono para API HTTP
- Vitest para testing
- Docker Compose para servicios locales
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 →