← Volver al listado de tecnologías

Setup Proyecto TypeScript

Por: SiempreListo
cqrstypescriptsetuparquitectura

Setup Proyecto TypeScript

En este capitulo configuraremos el proyecto OrderFlow con TypeScript. Crearemos la estructura de carpetas, las interfaces base y las clases abstractas que usaremos en los siguientes capitulos.

Inicialización

mkdir orderflow && cd orderflow
npm init -y
npm install typescript @types/node tsx -D
npx tsc --init

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "paths": {
      "@domain/*": ["./src/domain/*"],
      "@application/*": ["./src/application/*"],
      "@infrastructure/*": ["./src/infrastructure/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Estructura de Carpetas: Arquitectura por Capas

Organizamos el codigo siguiendo una arquitectura por capas que separa claramente las responsabilidades:

src/
├── domain/
│   ├── order/
│   │   ├── order.aggregate.ts
│   │   ├── order.events.ts
│   │   └── order.repository.ts
│   └── shared/
│       ├── aggregate-root.ts
│       └── domain-event.ts
├── application/
│   ├── commands/
│   │   ├── command.ts
│   │   ├── command-bus.ts
│   │   └── order/
│   │       ├── create-order.command.ts
│   │       └── create-order.handler.ts
│   └── queries/
│       ├── query.ts
│       ├── query-bus.ts
│       └── order/
│           ├── get-order.query.ts
│           └── get-order.handler.ts
├── infrastructure/
│   ├── persistence/
│   │   ├── postgres/
│   │   └── elasticsearch/
│   └── messaging/
│       └── event-bus.ts
└── index.ts

La estructura tiene tres capas principales:

Interfaces Base: Los Bloques Fundamentales

Definimos las clases e interfaces base que seran el fundamento de nuestra implementacion CQRS.

Aggregate Root: Base para Entidades de Dominio

El AggregateRoot es la clase base para todas las entidades raiz de nuestros agregados. Proporciona funcionalidad comun como la generacion de eventos de dominio:

// src/domain/shared/aggregate-root.ts
import { DomainEvent } from "./domain-event";

export abstract class AggregateRoot {
  private _events: DomainEvent[] = [];

  protected readonly _id: string;

  constructor(id: string) {
    this._id = id;
  }

  get id(): string {
    return this._id;
  }

  protected addEvent(event: DomainEvent): void {
    this._events.push(event);
  }

  pullEvents(): DomainEvent[] {
    const events = [...this._events];
    this._events = [];
    return events;
  }
}

El metodo addEvent permite que el agregado registre eventos cuando algo importante ocurre. El metodo pullEvents extrae esos eventos para publicarlos, limpiando la lista interna.

Domain Event: Comunicacion entre Modelos

Los eventos de dominio son la forma en que el Write Model comunica cambios al Read Model:

// src/domain/shared/domain-event.ts
export interface DomainEvent {
  readonly eventId: string;
  readonly aggregateId: string;
  readonly occurredAt: Date;
  readonly eventType: string;
}

export abstract class BaseDomainEvent implements DomainEvent {
  readonly eventId: string;
  readonly occurredAt: Date;

  constructor(readonly aggregateId: string) {
    this.eventId = crypto.randomUUID();
    this.occurredAt = new Date();
  }

  abstract get eventType(): string;
}

Cada evento tiene un eventId unico, sabe a que agregado pertenece (aggregateId), cuando ocurrio (occurredAt) y de que tipo es (eventType).

Command Base: Estructura de Comandos

// src/application/commands/command.ts
export interface Command {
  readonly commandId: string;
  readonly timestamp: Date;
}

export abstract class BaseCommand implements Command {
  readonly commandId: string;
  readonly timestamp: Date;

  constructor() {
    this.commandId = crypto.randomUUID();
    this.timestamp = new Date();
  }
}

export interface CommandHandler<T extends Command> {
  execute(command: T): Promise<void>;
}

Query Base: Estructura de Consultas

// src/application/queries/query.ts
export interface Query<TResult> {
  readonly queryId: string;
}

export abstract class BaseQuery<TResult> implements Query<TResult> {
  readonly queryId: string;

  constructor() {
    this.queryId = crypto.randomUUID();
  }
}

export interface QueryHandler<TQuery extends Query<TResult>, TResult> {
  execute(query: TQuery): Promise<TResult>;
}

Nota como Query<TResult> es generica: el tipo de resultado se define al crear queries especificas.

Dependencias del Proyecto

# Validación
npm install zod

# Base de datos
npm install pg @types/pg

# Elasticsearch
npm install @elastic/elasticsearch

# Mensajería (opcional)
npm install amqplib @types/amqplib

# Testing
npm install vitest -D

package.json Scripts

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest",
    "typecheck": "tsc --noEmit"
  }
}

Setup Equivalente en Go

mkdir orderflow && cd orderflow
go mod init github.com/company/orderflow
orderflow/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── domain/
│   │   └── order/
│   ├── application/
│   │   ├── command/
│   │   └── query/
│   └── infrastructure/
│       ├── postgres/
│       └── elasticsearch/
├── go.mod
└── go.sum

Setup Equivalente en Python

mkdir orderflow && cd orderflow
python -m venv .venv
source .venv/bin/activate
pip install poetry
poetry init
orderflow/
├── src/
│   └── orderflow/
│       ├── domain/
│       │   └── order/
│       ├── application/
│       │   ├── commands/
│       │   └── queries/
│       └── infrastructure/
├── tests/
├── pyproject.toml
└── poetry.lock
# pyproject.toml
[tool.poetry.dependencies]
python = "^3.12"
pydantic = "^2.0"
sqlalchemy = "^2.0"
elasticsearch = "^8.0"

Proximos Pasos

En el siguiente capitulo implementaremos el Command Bus para despachar comandos a sus handlers.


Glosario

Arquitectura por Capas

Definicion: Patron de organizacion de codigo donde el sistema se divide en capas con responsabilidades especificas y dependencias controladas. Las capas superiores dependen de las inferiores, pero no al reves.

Por que es importante: Facilita el mantenimiento y testing. Los cambios en infraestructura no afectan al dominio. Permite reemplazar implementaciones sin tocar la logica de negocio.

Ejemplo practico: domain/ no importa nada de infrastructure/. Si cambias de PostgreSQL a MongoDB, solo modificas infrastructure/, el dominio permanece intacto.


Clase Abstracta

Definicion: Clase que no puede instanciarse directamente, solo heredarse. Puede contener implementaciones parciales que las clases hijas completan.

Por que es importante: Permite definir comportamiento comun que todas las subclases comparten, mientras obliga a implementar metodos especificos.

Ejemplo practico: AggregateRoot es abstracta. No puedes crear new AggregateRoot(), pero OrderAggregate extends AggregateRoot hereda la funcionalidad de eventos.


Pull Events (Extraer Eventos)

Definicion: Patron donde los eventos generados por un agregado se acumulan internamente y se extraen en un momento posterior para publicarlos.

Por que es importante: Separa la generacion de eventos de su publicacion. El agregado se enfoca en logica de negocio; quien lo usa decide cuando publicar los eventos.

Ejemplo practico: order.confirm() agrega OrderConfirmedEvent internamente. Despues de guardar en BD, el handler llama order.pullEvents() para obtener y publicar los eventos pendientes.


Path Mapping (Alias de Rutas)

Definicion: Configuracion de TypeScript que permite usar alias como @domain/ en lugar de rutas relativas largas como ../../domain/.

Por que es importante: Mejora la legibilidad de los imports. Facilita reorganizar carpetas sin cambiar todos los imports.

Ejemplo practico: En lugar de import { Order } from "../../domain/order", usas import { Order } from "@domain/order".


Vitest

Definicion: Framework de testing moderno para JavaScript/TypeScript. Compatible con la API de Jest pero mas rapido gracias a la integracion con Vite.

Por que es importante: Los tests son esenciales en CQRS para verificar que commands y queries funcionan correctamente de forma aislada.

Ejemplo practico: Puedes escribir tests que envien commands y verifiquen que el agregado produce los eventos esperados.