codigo0/docs/SEPARACION_CAPAS_LOGICA_NEGOCIO.md
planetazuzu 5d7a6500fe refactor: Fase 1 - Clean Architecture, refactorización modular y eliminación de duplicidades
-  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
2026-01-25 21:09:47 +01:00

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