← Volver al listado de tecnologías
Capítulo 7: Organización del Código
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:
- ✅ Clara separación de capas
- ✅ Fácil de entender la arquitectura
- ✅ Escalable para proyectos grandes
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:
- ✅ Cohesión por feature
- ✅ Fácil de encontrar código relacionado
- ✅ Ideal para equipos por dominio
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:
- Estructura de Directorios: Por capas vs por features
- Convenciones de Nombres: Archivos, clases, variables
- Organización de Tests: Junto al código o separado
- Composición: Inyección de dependencias manual
- Módulos: Imports limpios con path aliases
- Configuración: tsconfig, package.json, variables de entorno
Principios clave:
- ✅ Estructura clara y predecible
- ✅ Nombres descriptivos y consistentes
- ✅ Separación de capas visible
- ✅ Fácil navegación por el código
- ✅ Configuración centralizada
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 Structure | Estructura de Directorios | Organización física del código en carpetas |
| Vertical Slice | Corte Vertical | Organización por features completas |
| Naming Convention | Convención de Nombres | Reglas consistentes para nombrar archivos y código |
| Path Alias | Alias de Ruta | Atajo para imports (ej: @domain) |
| Composition Root | Raíz de Composición | Lugar donde se ensambla la aplicación |
| Entry Point | Punto de Entrada | Archivo principal que inicia la app |
| Module | Módulo | Agrupación lógica de código relacionado |