← Volver al listado de tecnologías

Capítulo 7: Organización del Código

Por: Alfred Pennyworth
arquitectura-hexagonaltypescriptorganizacionestructurabest-practices

Capítulo 7: Organización del Código

Introducción

Una buena organización del código es fundamental para mantener la arquitectura hexagonal clara y comprensible. En este capítulo aprenderás estructuras de directorios, convenciones de nombres y cómo organizar un proyecto real.

1. Estructura de Directorios

Opción 1: Por Capas (Recomendada)

src/
├── domain/                      # Capa de Dominio
│   ├── user/
│   │   ├── user.entity.ts
│   │   ├── email.vo.ts
│   │   └── user-events.ts
│   ├── order/
│   │   ├── order.entity.ts
│   │   ├── order-item.vo.ts
│   │   └── order.aggregate.ts
│   └── shared/
│       ├── entity.base.ts
│       └── value-object.base.ts

├── application/                 # Capa de Aplicación
│   ├── user/
│   │   ├── ports/
│   │   │   ├── input/
│   │   │   │   ├── create-user.port.ts
│   │   │   │   └── get-user.port.ts
│   │   │   └── output/
│   │   │       ├── user-repository.port.ts
│   │   │       └── email-service.port.ts
│   │   └── use-cases/
│   │       ├── create-user.usecase.ts
│   │       └── get-user.usecase.ts
│   ├── order/
│   │   ├── ports/
│   │   │   ├── input/
│   │   │   │   └── place-order.port.ts
│   │   │   └── output/
│   │   │       ├── order-repository.port.ts
│   │   │       └── inventory-service.port.ts
│   │   └── use-cases/
│   │       └── place-order.usecase.ts
│   └── shared/
│       ├── dtos/
│       └── errors/
│           ├── application.error.ts
│           └── domain.error.ts

└── infrastructure/              # Capa de Infraestructura
    ├── adapters/
    │   ├── primary/             # Adaptadores Primarios
    │   │   ├── api/
    │   │   │   ├── user.controller.ts
    │   │   │   ├── order.controller.ts
    │   │   │   └── routes.ts
    │   │   └── cli/
    │   │       └── user.command.ts
    │   └── secondary/           # Adaptadores Secundarios
    │       ├── repositories/
    │       │   ├── postgres-user.repository.ts
    │       │   ├── postgres-order.repository.ts
    │       │   └── inmemory-user.repository.ts
    │       └── services/
    │           ├── sendgrid-email.service.ts
    │           └── http-inventory.service.ts
    ├── config/
    │   ├── database.config.ts
    │   └── app.config.ts
    ├── composition/             # Inyección de Dependencias
    │   ├── user.composition.ts
    │   ├── order.composition.ts
    │   └── app.composition.ts
    └── server.ts                # Punto de entrada

Ventajas:

Opción 2: Por Features (Vertical Slice)

src/
├── features/
│   ├── user/
│   │   ├── domain/
│   │   │   ├── user.entity.ts
│   │   │   └── email.vo.ts
│   │   ├── application/
│   │   │   ├── create-user.usecase.ts
│   │   │   └── get-user.usecase.ts
│   │   ├── infrastructure/
│   │   │   ├── user.controller.ts
│   │   │   └── postgres-user.repository.ts
│   │   └── ports/
│   │       ├── user-repository.port.ts
│   │       └── create-user.port.ts
│   └── order/
│       ├── domain/
│       ├── application/
│       ├── infrastructure/
│       └── ports/
└── shared/
    ├── domain/
    ├── infrastructure/
    └── types/

Ventajas:

2. Convenciones de Nombres

Archivos y Directorios

// ✅ BIEN: Nombres descriptivos y consistentes

// Entidades del dominio
user.entity.ts
order.entity.ts

// Value Objects
email.vo.ts
money.vo.ts

// Agregados
order.aggregate.ts

// Puertos
user-repository.port.ts
create-user.port.ts

// Casos de Uso
create-user.usecase.ts
place-order.usecase.ts

// Adaptadores
postgres-user.repository.ts
http-inventory.service.ts
user.controller.ts

// DTOs
create-user.dto.ts
user-response.dto.ts

// Tests
user.entity.spec.ts
create-user.usecase.spec.ts

Clases e Interfaces

// Entidades: PascalCase + Entity
class User {}
class Order {}

// Value Objects: PascalCase + VO (opcional)
class Email {}
class Money {}

// Interfaces de puertos: PascalCase + descriptivo
interface UserRepository {}
interface CreateUserUseCase {}
interface EmailService {}

// Implementaciones: PascalCase + tecnología + tipo
class PostgresUserRepository implements UserRepository {}
class InMemoryUserRepository implements UserRepository {}
class SendGridEmailService implements EmailService {}

// Casos de Uso: PascalCase + UseCase + Impl (opcional)
class CreateUserUseCase {}
class CreateUserUseCaseImpl implements CreateUserUseCase {}

// Controllers: PascalCase + Controller
class UserController {}
class OrderController {}

// DTOs: PascalCase + DTO/Command/Query
interface CreateUserCommand {}
interface UserResponseDTO {}
interface GetUserQuery {}

Variables y Métodos

// camelCase para variables y métodos
const userRepository = new PostgresUserRepository();
const emailService = new SendGridEmailService();

async function createUser() {}
async function findUserById() {}

// Prefijos comunes
const isValid = true;          // boolean: is/has/can
const hasPermission = false;
const canDelete = true;

const getUserById = () => {}   // funciones: verbo + sustantivo
const saveUser = () => {}
const deleteOrder = () => {}

3. Organización de Tests

src/
└── application/
    └── user/
        └── use-cases/
            ├── create-user.usecase.ts
            └── create-user.usecase.spec.ts  # Test junto al código

// Alternativa: carpeta tests separada
tests/
├── unit/
│   ├── domain/
│   │   └── user.entity.spec.ts
│   └── application/
│       └── create-user.usecase.spec.ts
├── integration/
│   └── repositories/
│       └── postgres-user.repository.spec.ts
└── e2e/
    └── api/
        └── user.api.spec.ts

4. Ejemplo Completo: E-commerce

Estructura del Proyecto

ecommerce-api/
├── src/
│   ├── domain/
│   │   ├── product/
│   │   │   ├── product.entity.ts
│   │   │   ├── price.vo.ts
│   │   │   └── sku.vo.ts
│   │   ├── order/
│   │   │   ├── order.aggregate.ts
│   │   │   ├── order-item.vo.ts
│   │   │   └── order-status.vo.ts
│   │   ├── customer/
│   │   │   ├── customer.entity.ts
│   │   │   └── address.vo.ts
│   │   └── shared/
│   │       ├── entity.base.ts
│   │       └── result.ts
│   │
│   ├── application/
│   │   ├── product/
│   │   │   ├── ports/
│   │   │   │   ├── input/
│   │   │   │   │   ├── create-product.port.ts
│   │   │   │   │   └── search-products.port.ts
│   │   │   │   └── output/
│   │   │   │       └── product-repository.port.ts
│   │   │   └── use-cases/
│   │   │       ├── create-product.usecase.ts
│   │   │       └── search-products.usecase.ts
│   │   ├── order/
│   │   │   ├── ports/
│   │   │   │   ├── input/
│   │   │   │   │   └── place-order.port.ts
│   │   │   │   └── output/
│   │   │   │       ├── order-repository.port.ts
│   │   │   │       └── payment-gateway.port.ts
│   │   │   └── use-cases/
│   │   │       └── place-order.usecase.ts
│   │   └── customer/
│   │       ├── ports/
│   │       └── use-cases/
│   │
│   ├── infrastructure/
│   │   ├── adapters/
│   │   │   ├── primary/
│   │   │   │   └── api/
│   │   │   │       ├── product.controller.ts
│   │   │   │       ├── order.controller.ts
│   │   │   │       ├── customer.controller.ts
│   │   │   │       └── routes.ts
│   │   │   └── secondary/
│   │   │       ├── repositories/
│   │   │       │   ├── postgres-product.repository.ts
│   │   │       │   ├── postgres-order.repository.ts
│   │   │       │   └── postgres-customer.repository.ts
│   │   │       └── services/
│   │   │           └── stripe-payment.gateway.ts
│   │   ├── config/
│   │   │   ├── database.ts
│   │   │   ├── server.ts
│   │   │   └── env.ts
│   │   ├── composition/
│   │   │   ├── product.composition.ts
│   │   │   ├── order.composition.ts
│   │   │   └── app.composition.ts
│   │   └── server.ts
│   │
│   └── shared/
│       ├── types/
│       ├── utils/
│       └── errors/

├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/

├── package.json
├── tsconfig.json
└── .env.example

Archivo de Composición (DI)

// infrastructure/composition/product.composition.ts
import { PostgresProductRepository } from '../adapters/secondary/repositories/postgres-product.repository';
import { CreateProductUseCase } from '../../application/product/use-cases/create-product.usecase';
import { ProductController } from '../adapters/primary/api/product.controller';

export function createProductModule(pool: any) {
  // Adaptador secundario
  const productRepository = new PostgresProductRepository(pool);

  // Caso de uso
  const createProductUseCase = new CreateProductUseCase(productRepository);

  // Adaptador primario
  const productController = new ProductController(createProductUseCase);

  return {
    productRepository,
    createProductUseCase,
    productController
  };
}
// infrastructure/composition/app.composition.ts
import { createProductModule } from './product.composition';
import { createOrderModule } from './order.composition';
import { createDatabasePool } from '../config/database';

export function composeApplication() {
  const pool = createDatabasePool();

  const productModule = createProductModule(pool);
  const orderModule = createOrderModule(pool, productModule.productRepository);

  return {
    productModule,
    orderModule
  };
}

Punto de Entrada

// infrastructure/server.ts
import express from 'express';
import { composeApplication } from './composition/app.composition';

const app = express();
app.use(express.json());

// Composición
const { productModule, orderModule } = composeApplication();

// Rutas
app.post('/products', (req, res) =>
  productModule.productController.create(req, res)
);

app.post('/orders', (req, res) =>
  orderModule.orderController.placeOrder(req, res)
);

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

5. Módulos y Paquetes

Organización por Módulos

// Cada módulo expone su API pública

// domain/user/index.ts
export { User } from './user.entity';
export { Email } from './email.vo';

// application/user/index.ts
export { CreateUserUseCase } from './use-cases/create-user.usecase';
export { type CreateUserCommand } from './ports/input/create-user.port';

// infrastructure/adapters/secondary/repositories/index.ts
export { PostgresUserRepository } from './postgres-user.repository';

Imports Limpios

// ❌ MAL: Imports profundos
import { User } from '../../../domain/user/user.entity';
import { Email } from '../../../domain/user/email.vo';

// ✅ BIEN: Import desde módulo
import { User, Email } from '@domain/user';

// ✅ BIEN: Path aliases en tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@domain/*": ["./src/domain/*"],
      "@application/*": ["./src/application/*"],
      "@infrastructure/*": ["./src/infrastructure/*"]
    }
  }
}

6. Configuración del Proyecto

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "paths": {
      "@domain/*": ["./src/domain/*"],
      "@application/*": ["./src/application/*"],
      "@infrastructure/*": ["./src/infrastructure/*"],
      "@shared/*": ["./src/shared/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

package.json

{
  "name": "ecommerce-api",
  "version": "1.0.0",
  "scripts": {
    "dev": "ts-node-dev src/infrastructure/server.ts",
    "build": "tsc",
    "start": "node dist/infrastructure/server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "dependencies": {
    "express": "^4.18.0",
    "pg": "^8.11.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.0.0",
    "jest": "^29.0.0",
    "ts-node-dev": "^2.0.0",
    "typescript": "^5.0.0"
  }
}

7. Variables de Entorno

# .env.example
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/ecommerce
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10

# Server
PORT=3000
NODE_ENV=development

# External Services
STRIPE_API_KEY=sk_test_xxx
SENDGRID_API_KEY=SG.xxx
// infrastructure/config/env.ts
import dotenv from 'dotenv';
dotenv.config();

interface Config {
  database: {
    url: string;
    poolMin: number;
    poolMax: number;
  };
  server: {
    port: number;
    env: string;
  };
  stripe: {
    apiKey: string;
  };
}

export const config: Config = {
  database: {
    url: process.env.DATABASE_URL!,
    poolMin: parseInt(process.env.DATABASE_POOL_MIN || '2'),
    poolMax: parseInt(process.env.DATABASE_POOL_MAX || '10')
  },
  server: {
    port: parseInt(process.env.PORT || '3000'),
    env: process.env.NODE_ENV || 'development'
  },
  stripe: {
    apiKey: process.env.STRIPE_API_KEY!
  }
};

8. Documentación del Código

JSDoc para Interfaces Públicas

/**
 * Repositorio para gestionar usuarios
 *
 * @interface UserRepository
 */
interface UserRepository {
  /**
   * Guarda un usuario en el repositorio
   *
   * @param user - Entidad de usuario a guardar
   * @returns Promise que resuelve cuando se guarda
   * @throws {DatabaseError} Si hay error de conexión
   */
  save(user: User): Promise<void>;

  /**
   * Busca un usuario por su ID
   *
   * @param id - ID del usuario
   * @returns Usuario encontrado o null
   */
  findById(id: string): Promise<User | null>;
}

README por Módulo

# Módulo de Usuarios

## Estructura

- `domain/`: Entidades y value objects
- `application/`: Casos de uso y puertos
- `infrastructure/`: Adaptadores

## Casos de Uso

- `CreateUserUseCase`: Crea nuevo usuario
- `GetUserUseCase`: Obtiene usuario por ID

## Adaptadores

- `PostgresUserRepository`: Persistencia en PostgreSQL
- `UserController`: API REST

9. Conclusión

En este capítulo aprendiste:

  1. Estructura de Directorios: Por capas vs por features
  2. Convenciones de Nombres: Archivos, clases, variables
  3. Organización de Tests: Junto al código o separado
  4. Composición: Inyección de dependencias manual
  5. Módulos: Imports limpios con path aliases
  6. Configuración: tsconfig, package.json, variables de entorno

Principios clave:

En el próximo capítulo veremos estrategias de testing para arquitectura hexagonal.

Glosario del Capítulo

Término (Inglés)Término (Español)Definición
Directory StructureEstructura de DirectoriosOrganización física del código en carpetas
Vertical SliceCorte VerticalOrganización por features completas
Naming ConventionConvención de NombresReglas consistentes para nombrar archivos y código
Path AliasAlias de RutaAtajo para imports (ej: @domain)
Composition RootRaíz de ComposiciónLugar donde se ensambla la aplicación
Entry PointPunto de EntradaArchivo principal que inicia la app
ModuleMóduloAgrupación lógica de código relacionado

Referencias