- ✅ Ticket 1.1: Estructura Clean Architecture en backend - ✅ Ticket 1.2: Schemas Zod compartidos - ✅ Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares) - ✅ Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares) - ✅ Ticket 1.5: Eliminación de duplicidades (~50 líneas) Cambios principales: - Creada estructura Clean Architecture en backend/src/ - Schemas Zod compartidos en backend/src/shared/schemas/ - Refactorización modular de drugs y procedures - Utilidades genéricas en src/utils/ (filter, validation) - Eliminados scripts obsoletos y documentación antigua - Corregidos errores: QueryClient, import test-error-handling - Build verificado y funcionando correctamente
13 KiB
13 KiB
🏗️ Separación de Capas y Organización de Lógica de Negocio
🎯 Objetivo
Estudiar y definir cómo separar las capas de la aplicación y organizar la lógica de negocio siguiendo Clean Architecture.
📐 Arquitectura en Capas
Estructura Propuesta
┌─────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (Routes) │
│ - Express Routes │
│ - Middleware (auth, validation, rate-limit) │
│ - Request/Response DTOs │
│ - NO lógica de negocio │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ APPLICATION LAYER (Use Cases) │
│ - Services (orquestación) │
│ - Use Cases (casos de uso específicos) │
│ - DTOs de aplicación │
│ - Validaciones de aplicación │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ DOMAIN LAYER (Core) │
│ - Entities (entidades de negocio) │
│ - Value Objects (objetos de valor) │
│ - Domain Services (lógica de dominio pura) │
│ - Repository Interfaces (contratos) │
│ - Domain Events │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER (Implementación) │
│ - Repository Implementations │
│ - Database Access │
│ - File Storage │
│ - External Services │
│ - Cache │
└─────────────────────────────────────────────────────────┘
📁 Organización de Carpetas
backend/src/
├── domain/ # 🎯 DOMAIN LAYER
│ ├── entities/ # Entidades de negocio
│ │ ├── ContentItem.ts
│ │ ├── Drug.ts
│ │ ├── Protocol.ts
│ │ ├── GlossaryTerm.ts
│ │ └── MedicalReview.ts
│ │
│ ├── value-objects/ # Objetos de valor inmutables
│ │ ├── ContentStatus.ts
│ │ ├── ContentPriority.ts
│ │ ├── DoseRange.ts
│ │ ├── PatientAge.ts
│ │ ├── PatientWeight.ts
│ │ └── Version.ts
│ │
│ ├── services/ # Servicios de dominio
│ │ ├── CriticalErrorDetector.ts
│ │ ├── DoseCalculator.ts
│ │ └── ProtocolValidator.ts
│ │
│ ├── repositories/ # Interfaces de repositorios
│ │ ├── IContentRepository.ts
│ │ ├── IDrugRepository.ts
│ │ ├── IGlossaryRepository.ts
│ │ └── IMediaRepository.ts
│ │
│ └── events/ # Eventos de dominio
│ ├── ContentSubmitted.ts
│ ├── ContentApproved.ts
│ └── ContentPublished.ts
│
├── application/ # 🔧 APPLICATION LAYER
│ ├── services/ # Servicios de aplicación
│ │ ├── ContentService.ts
│ │ ├── DrugService.ts
│ │ ├── DoseValidationService.ts
│ │ ├── ProtocolValidationService.ts
│ │ ├── MedicalValidationService.ts
│ │ └── MediaService.ts
│ │
│ ├── use-cases/ # Casos de uso específicos
│ │ ├── content/
│ │ │ ├── CreateContentUseCase.ts
│ │ │ ├── UpdateContentUseCase.ts
│ │ │ ├── SubmitForReviewUseCase.ts
│ │ │ └── PublishContentUseCase.ts
│ │ │
│ │ ├── drugs/
│ │ │ ├── CalculateDoseUseCase.ts
│ │ │ ├── ValidateDoseUseCase.ts
│ │ │ └── CreateDrugUseCase.ts
│ │ │
│ │ └── protocols/
│ │ ├── ExecuteProtocolUseCase.ts
│ │ └── ValidateProtocolSequenceUseCase.ts
│ │
│ └── dto/ # Data Transfer Objects
│ ├── CreateContentDTO.ts
│ ├── UpdateContentDTO.ts
│ └── DoseCalculationDTO.ts
│
├── infrastructure/ # 🔌 INFRASTRUCTURE LAYER
│ ├── repositories/ # Implementaciones de repositorios
│ │ ├── ContentRepository.ts
│ │ ├── DrugRepository.ts
│ │ ├── GlossaryRepository.ts
│ │ └── MediaRepository.ts
│ │
│ ├── database/ # Acceso a base de datos
│ │ ├── Database.ts
│ │ ├── migrations/
│ │ └── queries/
│ │
│ ├── storage/ # Almacenamiento de archivos
│ │ ├── FileStorage.ts
│ │ └── MediaStorage.ts
│ │
│ ├── cache/ # Sistema de caché
│ │ └── CacheService.ts
│ │
│ └── external/ # Servicios externos
│ └── NotificationService.ts
│
└── presentation/ # 🌐 PRESENTATION LAYER
├── routes/ # Rutas Express
│ ├── content.ts
│ ├── drugs.ts
│ ├── glossary.ts
│ ├── media.ts
│ └── validation.ts
│
├── middleware/ # Middleware
│ ├── auth.ts
│ ├── validate.ts
│ └── rate-limit.ts
│
└── validators/ # Validadores Zod
├── content.ts
├── drugs.ts
└── glossary.ts
🔄 Flujo de Datos
Ejemplo: Crear Contenido
1. Request → Route (presentation/routes/content.ts)
↓
2. Validar con Zod (presentation/validators/content.ts)
↓
3. Llamar Use Case (application/use-cases/content/CreateContentUseCase.ts)
↓
4. Use Case llama Service (application/services/ContentService.ts)
↓
5. Service usa Domain Entities (domain/entities/ContentItem.ts)
↓
6. Service llama Repository Interface (domain/repositories/IContentRepository.ts)
↓
7. Repository Implementation (infrastructure/repositories/ContentRepository.ts)
↓
8. Database (infrastructure/database/Database.ts)
↓
9. Response ← Route
📝 Reglas de Separación
✅ Domain Layer
- SÍ: Entidades, Value Objects, Lógica de negocio pura
- SÍ: Interfaces de repositorios
- NO: Acceso a base de datos
- NO: Dependencias externas
- NO: Frameworks (Express, etc.)
✅ Application Layer
- SÍ: Casos de uso, Orquestación
- SÍ: Validaciones de aplicación
- SÍ: DTOs de aplicación
- NO: Lógica de dominio (debe estar en Domain)
- NO: Acceso directo a base de datos
✅ Infrastructure Layer
- SÍ: Implementaciones de repositorios
- SÍ: Acceso a base de datos
- SÍ: Servicios externos
- NO: Lógica de negocio
- NO: Validaciones de dominio
✅ Presentation Layer
- SÍ: Routes, Middleware
- SÍ: Validación de entrada (Zod)
- SÍ: Transformación Request/Response
- NO: Lógica de negocio
- NO: Acceso directo a repositorios
🎯 Ejemplo de Implementación
Domain Entity (Inmutable)
// domain/entities/ContentItem.ts
export class ContentItem {
private constructor(
readonly id: string,
readonly type: ContentType,
readonly title: string,
readonly status: ContentStatus,
readonly createdAt: Date,
readonly updatedAt: Date
) {}
static create(
id: string,
type: ContentType,
title: string
): ContentItem {
// Validaciones de dominio
if (!title || title.trim().length === 0) {
throw new Error('Título es obligatorio');
}
return new ContentItem(
id,
type,
title,
ContentStatus.DRAFT,
new Date(),
new Date()
);
}
submitForReview(): ContentItem {
if (this.status !== ContentStatus.DRAFT) {
throw new Error('Solo contenido en borrador puede enviarse a revisión');
}
return new ContentItem(
this.id,
this.type,
this.title,
ContentStatus.IN_REVIEW,
this.createdAt,
new Date()
);
}
// Inmutable: siempre retorna nueva instancia
}
Use Case
// application/use-cases/content/CreateContentUseCase.ts
export class CreateContentUseCase {
constructor(
private readonly contentRepository: IContentRepository,
private readonly validator: ContentValidator
) {}
async execute(input: CreateContentDTO): Promise<ContentItem> {
// 1. Validar entrada
this.validator.validateCreate(input);
// 2. Crear entidad de dominio
const content = ContentItem.create(
crypto.randomUUID(),
input.type,
input.title
);
// 3. Persistir
await this.contentRepository.save(content);
// 4. Retornar
return content;
}
}
Repository Interface (Domain)
// domain/repositories/IContentRepository.ts
export interface IContentRepository {
findById(id: string): Promise<ContentItem | null>;
findAll(filters: ContentFilters): Promise<ContentItem[]>;
save(content: ContentItem): Promise<ContentItem>;
delete(id: string): Promise<void>;
}
Repository Implementation (Infrastructure)
// infrastructure/repositories/ContentRepository.ts
export class ContentRepository implements IContentRepository {
constructor(private readonly db: Database) {}
async findById(id: string): Promise<ContentItem | null> {
const result = await this.db.query(
'SELECT * FROM tes_content.content_items WHERE id = $1',
[id]
);
if (result.rows.length === 0) return null;
return this.mapToDomain(result.rows[0]);
}
async save(content: ContentItem): Promise<ContentItem> {
// Mapear de dominio a persistencia
const row = this.mapToPersistence(content);
await this.db.query(
`INSERT INTO tes_content.content_items (...) VALUES (...)`,
[row]
);
return content;
}
private mapToDomain(row: any): ContentItem {
// Mapear de persistencia a dominio
}
private mapToPersistence(content: ContentItem): any {
// Mapear de dominio a persistencia
}
}
Route (Presentation)
// presentation/routes/content.ts
router.post(
'/',
authenticate,
requirePermission('content:write'),
validateBody(createContentSchema),
async (req: AuthRequest, res: Response) => {
try {
const useCase = new CreateContentUseCase(
contentRepository,
contentValidator
);
const content = await useCase.execute(req.body);
res.status(201).json({
id: content.id,
title: content.title,
status: content.status.toString()
});
} catch (error) {
handleError(error, res);
}
}
);
🔒 Reglas de Dependencias
Regla de Dependencia
- Domain: No depende de nadie
- Application: Solo depende de Domain
- Infrastructure: Depende de Domain y Application
- Presentation: Depende de Application y Domain
Diagrama de Dependencias
Presentation → Application → Domain
↓ ↓
Infrastructure → Domain
✅ Checklist de Separación
Al crear nuevo código, verificar:
- ¿Está en la capa correcta?
- ¿Depende solo de capas inferiores?
- ¿La lógica de negocio está en Domain?
- ¿Los casos de uso están en Application?
- ¿Las implementaciones están en Infrastructure?
- ¿Las rutas están en Presentation?
Fin del documento