API REST completa para gestión de usuarios implementada con Domain-Driven Design (DDD) en TypeScript y MySQL.
Este proyecto está diseñado específicamente para aprender y entender Domain-Driven Design (DDD). Cada archivo contiene comentarios detallados explicando:
- Por qué se usa cada patrón
- Qué problema resuelve
- Cómo se relaciona con DDD
- Mejores prácticas aplicadas
El proyecto sigue una arquitectura en capas basada en DDD:
src/
├── domain/ # Capa de Dominio (Núcleo del negocio)
│ ├── entities/ # Entidades (User)
│ ├── value-objects/ # Value Objects (UserId, Email, UserName)
│ ├── repositories/ # Interfaces de repositorios
│ ├── services/ # Servicios de dominio
│ └── exceptions/ # Excepciones del dominio
│
├── application/ # Capa de Aplicación (Casos de uso)
│ ├── use-cases/ # Casos de uso (CreateUser, GetUser, etc.)
│ ├── dtos/ # Data Transfer Objects
│ └── mappers/ # Mappers entre capas
│
├── infrastructure/ # Capa de Infraestructura (Detalles técnicos)
│ ├── database/ # Conexión y configuración de BD
│ │ ├── migrations/ # Migraciones de Sequelize
│ │ ├── seeders/ # Seeders de datos de prueba
│ │ └── connection.ts
│ └── repositories/ # Implementaciones de repositorios
│
├── presentation/ # Capa de Presentación (API REST)
│ ├── controllers/ # Controladores HTTP
│ ├── routes/ # Definición de rutas
│ ├── middlewares/ # Middlewares de Express
│ └── validators/ # Validadores de entrada
│
├── shared/ # Código compartido
│ ├── Result.ts # Patrón Result para manejo de errores
│ └── Guard.ts # Validaciones reutilizables
│
└── server.ts # Punto de entrada y Composition Root
- Domain: Lógica de negocio pura, independiente de tecnología
- Application: Orquestación de casos de uso
- Infrastructure: Detalles de implementación (BD, APIs externas)
- Presentation: Adaptadores para la interfaz (REST API)
- Entities: User (con identidad única)
- Value Objects: UserId, Email, UserName (inmutables, sin identidad)
- Aggregates: User actúa como Aggregate Root
- Repositories: Abstracción de persistencia
- Domain Services: Lógica que no pertenece a entidades
- Dependency Inversion: Las capas dependen de abstracciones
- Repository Pattern: Abstracción de persistencia
- Use Case Pattern: Un caso de uso = una operación de negocio
- Factory Methods: Creación controlada de objetos
- Result Pattern: Manejo funcional de errores
- Guard Clauses: Validaciones defensivas
- Node.js (v16 o superior)
- MySQL (v5.7 o superior)
- npm o yarn
npm installCopia el archivo .env.example a .env:
cp .env.example .envEdita el archivo .env con tus credenciales de MySQL:
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=tu_password
DB_NAME=ddd_users_dbOpción A: Usando Sequelize (Recomendado)
# Crear la base de datos manualmente primero
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS ddd_users_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Ejecutar migraciones y seeders
npm run db:setupOpción B: Usando el script SQL directo
# Conecta a MySQL
mysql -u root -p
# Ejecuta el script
source src/infrastructure/database/schema.sqlModo desarrollo (con auto-reload):
npm run devModo producción:
npm run build
npm startEl servidor estará disponible en: http://localhost:3000
# Ejecutar todas las migraciones pendientes
npm run db:migrate
# Revertir la última migración
npm run db:migrate:undo
# Revertir todas las migraciones
npm run db:migrate:undo:all# Ejecutar todos los seeders (insertar datos de prueba)
npm run db:seed
# Revertir todos los seeders (eliminar datos de prueba)
npm run db:seed:undo# Ejecutar migraciones + seeders en un solo comando
npm run db:setupEl seeder crea 15 usuarios con diferentes perfiles:
- Usuarios activos e inactivos
- Diferentes rangos de edad (13-95 años)
- Nombres con caracteres especiales
- IDs predecibles para pruebas
Algunos IDs de ejemplo:
550e8400-e29b-41d4-a716-446655440001- Juan Pérez550e8400-e29b-41d4-a716-446655440002- María García550e8400-e29b-41d4-a716-446655440003- Carlos López (inactivo)
GET /POST /api/users
Content-Type: application/json
{
"name": "Juan Pérez",
"email": "juan.perez@example.com",
"age": 25
}GET /api/usersGET /api/users/{id}PUT /api/users/{id}
Content-Type: application/json
{
"name": "Juan Carlos Pérez",
"email": "juancarlos@example.com",
"age": 26,
"isActive": true
}DELETE /api/users/{id}Nota: El borrado es lógico (soft delete). El usuario no se elimina físicamente; se marca con deleted_at. Las lecturas (GET) excluyen usuarios con deleted_at definido.
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "María García",
"email": "maria.garcia@example.com",
"age": 30
}'curl http://localhost:3000/api/userscurl -X PUT http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440001 \
-H "Content-Type: application/json" \
-d '{
"name": "María García López",
"age": 31
}'curl -X DELETE http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440001Después del borrado, GET /api/users y GET /api/users/{id} ya no mostrarán ese usuario. La fila permanece en la BD con deleted_at establecido para auditoría.
-
Presentation Layer (userRoutes.ts)
- Recibe la petición HTTP
- Ejecuta validación con
validateCreateUser - Llama al controlador
-
Presentation Layer (UserController.ts)
- Extrae el DTO del body
- Llama al Use Case correspondiente
-
Application Layer (CreateUserUseCase.ts)
- Recibe el DTO
- Lo convierte a entidad de dominio usando UserMapper
- Valida reglas de negocio con UserDomainService
- Persiste usando el repositorio
- Retorna DTO de respuesta
-
Domain Layer
- Value Objects: Validan email y nombre
- Entity: User se crea con factory method
- Domain Service: Verifica que el email sea único
-
Infrastructure Layer (MySQLUserRepository.ts)
- Recibe la entidad User
- La convierte a formato de BD
- Ejecuta INSERT en MySQL
// Domain Layer define la INTERFAZ (abstracción)
interface IUserRepository {
save(user: User): Promise<void>;
}
// Application Layer DEPENDE de la abstracción
class CreateUserUseCase {
constructor(private repo: IUserRepository) {}
}
// Infrastructure Layer IMPLEMENTA la abstracción
class MySQLUserRepository implements IUserRepository {
save(user: User): Promise<void> { /* ... */ }
}
// Composition Root CONECTA todo
const repository = new MySQLUserRepository();
const useCase = new CreateUserUseCase(repository);Beneficio: Podemos cambiar MySQL por PostgreSQL sin tocar el dominio.
Entidad (User):
- Tiene identidad única (UserId)
- Mutable (puede cambiar con el tiempo)
- Se compara por ID
Value Object (Email):
- Sin identidad
- Inmutable
- Se compara por valor
User es un Aggregate Root porque:
- Define un límite transaccional
- Protege invariantes del agregado
- Es el punto de entrada para operaciones
Abstrae la persistencia:
// En lugar de escribir SQL directamente
db.query("SELECT * FROM users WHERE id = ?", [id]);
// Usamos el repositorio
userRepository.findById(userId);Cada Use Case = Una operación que un usuario puede hacer:
- CreateUserUseCase: "Como usuario, quiero registrarme"
- GetUserByIdUseCase: "Como admin, quiero ver un usuario"
Maneja errores sin excepciones:
const nameResult = UserName.create("Juan");
if (nameResult.isFailure) {
return Result.fail(nameResult.getError());
}
const name = nameResult.getValue();- TypeScript: Tipado estático y características avanzadas
- Express.js: Framework web minimalista
- MySQL: Base de datos relacional
- mysql2: Driver de MySQL para Node.js
- uuid: Generación de IDs únicos
- dotenv: Gestión de variables de entorno
-
SOLID Principles
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
-
Clean Code
- Nombres descriptivos
- Funciones pequeñas y enfocadas
- Comentarios explicativos
- Separación de concerns
-
Type Safety
- Uso extensivo de TypeScript
- Interfaces bien definidas
- Validación en tiempo de compilación
-
Error Handling
- Excepciones de dominio tipadas
- Middleware centralizado de errores
- Mensajes claros y accionables
- Campos requeridos
- Tipos de datos
- Longitudes básicas
- Formato de email RFC 5321
- Nombres con caracteres válidos
- Edad en rango válido
- Invariantes del agregado
- Reglas de negocio complejas
- Paginación: Implementar en GetAllUsersUseCase
- Eventos de Dominio: Para comunicación desacoplada
- CQRS: Separar modelos de lectura y escritura
- Event Sourcing: Guardar eventos en lugar de estado
- Testing: Unit tests, integration tests
- Autenticación: JWT, OAuth
- Documentación: Swagger/OpenAPI
- Logging: Winston, Morgan
- Monitoreo: Prometheus, Grafana
- Libro: "Domain-Driven Design" - Eric Evans
- Libro: "Implementing Domain-Driven Design" - Vaughn Vernon
- Artículos: Martin Fowler's blog
- Cursos: Pluralsight, Udemy
Este es un proyecto educativo. Siéntete libre de:
- Hacer fork
- Agregar features siguiendo DDD
- Mejorar documentación
- Compartir con otros desarrolladores
ISC
Desarrollador web senior especializado en Node.js, DDD y arquitectura de software.
¡Disfruta aprendiendo DDD! 🎉