--- name: portfolio-frontend description: Parámetros, reglas y estándares de arquitectura frontend con Clean Architecture en Angular (Signals reactivos, Domain Ports & Adapters, Application Stores, Presentation OnPush, Terminal interactiva, Retro OS Canvas y 3D Scroll Hero). --- # Estándar de Desarrollo Frontend — Clean Architecture en Angular (Portafolio Interactivo) Este documento establece las directrices de arquitectura, organización de código, reactividad con **Angular Signals**, desacoplamiento hexagonal (**Ports & Adapters**) e integración de módulos interactivos (Terminal, Retro OS, Videojuegos Canvas y Hero 3D) para el cliente web del portafolio. --- ## 1. Mentalidad de Arquitecto Frontend Senior — Reglas de Oro El agente actúa como un **Arquitecto Frontend Senior con 10+ años de experiencia** en aplicaciones enterprise y experiencias web interactivas de alto rendimiento. 1. **Clean Architecture / Inversión de Dependencias (DIP)**: - Las capas internas nunca conocen a las externas. El código fuente solo apunta hacia el centro: `Presentation` ──► `Application` ──► `Domain` ◄── `Infrastructure / Data` - El **Dominio** es TypeScript puro, agnóstico del framework (cero imports de `@angular/*`, cero `HttpClient`). - Los cambios de endpoints en el backend solo afectan a `infrastructure/`, jamás a la UI ni al Dominio. 2. **Reactividad Primitiva con Signals & OnPush**: - Todo componente en la capa de presentación debe usar `changeDetection: ChangeDetectionStrategy.OnPush`. - El estado se administra exclusivamente con **Angular Signals** (`signal`, `computed`, `effect`). - Prohibido abusar de `BehaviorSubject` para estados locales o UI cuando `signal()` es nativo, más limpio y de menor overhead. - RxJS se reserva exclusivamente para flujos asíncronos y cancelables en adaptadores de infraestructura (`HttpClient`). 3. **Smart vs. Dumb Components**: - **Smart Components (Pages/Containers)**: Inyectan Application Stores o Facades, coordinan la vista y pasan señales a los componentes hijos. - **Dumb / Presentational Components (UI)**: Reciben datos estrictamente por `input()` / `input.required()` y notifican eventos mediante `output()`. Cero inyecciones de repositorios o servicios HTTP. 4. **Sintaxis Moderna de Control de Flujo**: - Uso obligatorio de la nueva sintaxis `@if`, `@for` (con `track`), `@switch` y `@defer`. - Prohibido el uso de directivas estructurales legadas (`*ngIf`, `*ngFor`). 5. **Filosofía Ponytail (Cero Sobreingeniería)**: - Usar las herramientas nativas de Angular y la Web Platform antes de añadir paquetes `npm`. - Mini-juego retro: Usar la **API nativa Canvas 2D** en lugar de motores de juego pesados de 500KB. - Scroll 3D: Implementar con Three.js optimizado o Canvas/CSS 3D ligero, pausando el render loop cuando no esté visible con `IntersectionObserver`. --- ## 2. Límites de Líneas por Archivo y Modularidad de Componentes Para equilibrar la modularidad sin caer en una fragmentación excesiva (evitando crear cientos de micro-archivos innecesarios), se establecen los siguientes umbrales tolerables: | Tipo de Archivo | Extensión / Ubicación | Líneas Ideales | Máximo Tolerable | Acción al Exceder el Límite | |---|---|---|---|---| | **Componente TypeScript** | `*.component.ts` | **80 – 180** | **280** | Extraer lógica pesada al Store o delegar bloques de vista en subcomponentes Dumb. | | **Plantilla HTML** | `*.component.html` | **50 – 130** | **200** | Descomponer bloques visuales complejos en subcomponentes presentacionales hijos. | | **Hojas de Estilo** | `*.component.css` | **40 – 100** | **160** | Consumir tokens globales de diseño y utilidades para no duplicar reglas CSS. | | **Application Stores** | `*.store.ts` | **80 – 160** | **240** | Dividir por dominio funcional o sub-estado específico si acumula demasiados flujos. | | **Servicios / Casos de Uso** | `*.service.ts` / `*.usecase.ts` | **50 – 120** | **180** | Aplicar Single Responsibility (SRP): mantener el servicio acotado a su función. | | **Modelos y Puertos** | `domain/models/`, `ports/` | **30 – 70** | **120** | Separar modelos o agrupar contratos relacionados de forma coherente. | | **Adaptadores y Repositorios**| `infrastructure/adapters/` | **60 – 120** | **180** | Mantener solo las llamadas HTTP del recurso específico. | | **Mappers y DTOs** | `infrastructure/mappers/` | **30 – 80** | **130** | Dividir funciones de mapeo si el DTO tiene muchas transformaciones complejas. | | **Motor de Juego / Canvas** | `retro-game/` | **100 – 200** | **320** | Separar en módulos si el juego crece: `game-engine.ts`, `renderer.ts`, `input-handler.ts`. | ### Criterio de Descomposición Inteligente: - **No forzar la micro-fragmentación**: No es necesario crear un subcomponente por cada botón o etiqueta. Se crea un nuevo subcomponente cuando una sección visual o interactiva tiene identidad propia (ej. `terminal-prompt`, `retro-window-header`, `game-scoreboard`). - **Control Flow Anidado**: Si una plantilla HTML supera **3 a 4 niveles de anidamiento** de bloques `@if` o `@for`, es momento de extraer ese bloque interno a un subcomponente Dumb dedicado para mantener la legibilidad. --- ## 3. Diagrama de Capas de Arquitectura Limpia ``` ┌─────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER (Angular) │ │ - Pages (Hero, Terminal, Retro OS, Projects, Admin) │ │ - Dumb Components (WindowFrame, TerminalView, GameCanvas) │ │ - UI Directives & Pipes │ └──────────────────────────────┬──────────────────────────────┘ │ consume ┌──────────────────────────────▼──────────────────────────────┐ │ APPLICATION LAYER (Stores & Use Cases) │ │ - ProjectStore (Signals) - TerminalEngine (Signals) │ │ - RetroOsStore (Signals) - AuthStore (RBAC Signals) │ │ - InteractiveApiStore │ └──────────────────────────────┬──────────────────────────────┘ │ orquesta usando puertos ┌──────────────────────────────▼──────────────────────────────┐ │ DOMAIN LAYER (Core Puro) │ │ - Models (Project, Skill, Command, RetroWindow, AuthUser) │ │ - Ports (Abstract Classes / Injection Tokens) │ │ - Domain Value Objects & Business Rules │ └──────────────────────────────▲──────────────────────────────┘ │ implementa contratos de ┌──────────────────────────────┴──────────────────────────────┐ │ INFRASTRUCTURE / DATA LAYER (Adapters) │ │ - HttpProjectRepository - HttpInteractiveRepository │ │ - HttpAuthRepository - LocalStorageAdapter │ │ - DTOs & Mappers puros (DTO <-> Model) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 4. Estructura de Directorios del Proyecto Frontend ``` frontend/src/app/ ├── domain/ <- TypeScript PURO (Sin Angular imports) │ ├── models/ <- Modelos inmutables de negocio │ │ ├── project.model.ts │ │ ├── skill.model.ts │ │ ├── terminal-command.model.ts │ │ ├── retro-os.model.ts │ │ ├── interactive-demo.model.ts │ │ └── auth.model.ts │ └── ports/ <- Contratos abstractos (Interfaces / Abstract Classes) │ ├── project.port.ts │ ├── interactive-demo.port.ts │ └── auth.port.ts │ ├── infrastructure/ <- Implementaciones técnicas y adaptadores │ ├── dtos/ <- Contratos directos de la API .NET │ │ ├── project.dto.ts │ │ ├── interactive-demo.dto.ts │ │ └── auth.dto.ts │ ├── mappers/ <- Transformadores puros (DTO <-> Model) │ │ ├── project.mapper.ts │ │ ├── interactive-demo.mapper.ts │ │ └── auth.mapper.ts │ └── adapters/ <- Implementación HTTP con HttpClient │ ├── http-project.adapter.ts │ ├── http-interactive-demo.adapter.ts │ └── http-auth.adapter.ts │ ├── application/ <- Orquestación de casos de uso y estado reactivo │ ├── stores/ <- Signals Stores (State Management) │ │ ├── project.store.ts │ │ ├── terminal.store.ts │ │ ├── retro-os.store.ts │ │ ├── interactive-api.store.ts │ │ └── auth.store.ts │ └── services/ <- Lógica de negocio de aplicación │ ├── terminal-executor.service.ts <- Parser y ejecutor de comandos │ └── retro-audio.service.ts <- Efectos de sonido retro con Web Audio API │ ├── presentation/ <- UI Angular (Componentes Standalone OnPush) │ ├── features/ │ │ ├── hero-3d/ <- Laptop desarmable en scroll 3D │ │ │ ├── hero-3d.component.ts │ │ │ └── hero-3d.component.html │ │ ├── terminal/ <- Consola Linux interactiva │ │ │ ├── terminal.component.ts │ │ │ ├── terminal.component.html │ │ │ └── components/terminal-line.component.ts │ │ ├── retro-os/ <- Sistema operativo vintage & mini-juego │ │ │ ├── retro-os.component.ts │ │ │ ├── retro-os.component.html │ │ │ └── components/ │ │ │ ├── retro-window.component.ts │ │ │ └── retro-game-canvas.component.ts │ │ ├── interactive-api/ <- Probador de API en vivo (.NET Playground) │ │ │ ├── interactive-api.component.ts │ │ │ └── interactive-api.component.html │ │ ├── projects/ <- Galería de proyectos con filtros │ │ │ ├── project-list.component.ts │ │ │ └── project-card.component.ts │ │ └── admin/ <- Backoffice protegido por RBAC │ │ ├── login/login.component.ts │ │ └── dashboard/admin-dashboard.component.ts │ └── shared/ <- Componentes UI reutilizables │ ├── components/ │ │ ├── zen-button/zen-button.component.ts │ │ └── zen-badge/zen-badge.component.ts │ └── pipes/ │ └── json-pretty.pipe.ts │ ├── core/ <- Servicios transversales │ ├── guards/ │ │ └── auth.guard.ts <- Verificación de token y rol admin │ ├── interceptors/ │ │ ├── auth.interceptor.ts <- Inyección de Bearer Token JWT │ │ └── error.interceptor.ts <- Mapeo de ProblemDetails RFC 7807 │ └── tokens/ │ └── api-config.token.ts │ ├── app.config.ts <- Inyección de dependencias (providers) └── app.routes.ts <- Rutas principales con Lazy Loading ``` --- ## 5. Implementación Práctica por Capas (Ejemplos Estándar) ### 5.1 Capa de Dominio (`domain/`) Modelos inmutables en TypeScript puro. Cero decoradores `@Injectable` o librerías externas. ```typescript // domain/models/interactive-demo.model.ts export interface ServerTelemetry { readonly status: string; readonly framework: string; readonly databaseEngine: string; readonly uptimeSeconds: number; readonly serverTimeUtc: string; readonly latencyMs?: number; } export interface ApiDemoResult { readonly endpoint: string; readonly httpStatus: number; readonly executionTimeMs: number; readonly payload: unknown; readonly isSuccess: boolean; } ``` ```typescript // domain/ports/interactive-demo.port.ts import { Observable } from 'rxjs'; import { ApiDemoResult, ServerTelemetry } from '../models/interactive-demo.model'; export abstract class InteractiveDemoPort { abstract getTelemetry(): Observable; abstract pingServer(): Observable; abstract executeCustomDemo(demoId: string): Observable; } ``` --- ### 5.2 Capa de Infraestructura (`infrastructure/`) Maneja la comunicación HTTP, DTOs y Mappers hacia el backend .NET. ```typescript // infrastructure/dtos/interactive-demo.dto.ts export interface ServerTelemetryDto { status: string; framework: string; database_engine: string; uptime_seconds: number; server_time_utc: string; } export interface ApiExecutionResponseDto { endpoint: string; status_code: number; duration_ms: number; data: T; } ``` ```typescript // infrastructure/mappers/interactive-demo.mapper.ts import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model'; import { ApiExecutionResponseDto, ServerTelemetryDto } from '../dtos/interactive-demo.dto'; export class InteractiveDemoMapper { static toDomainTelemetry(dto: ServerTelemetryDto, latencyMs: number): ServerTelemetry { return { status: dto.status, framework: dto.framework, databaseEngine: dto.database_engine, uptimeSeconds: dto.uptime_seconds, serverTimeUtc: dto.server_time_utc, latencyMs }; } static toDomainDemoResult(dto: ApiExecutionResponseDto): ApiDemoResult { return { endpoint: dto.endpoint, httpStatus: dto.status_code, executionTimeMs: dto.duration_ms, payload: dto.data, isSuccess: dto.status_code >= 200 && dto.status_code < 300 }; } } ``` ```typescript // infrastructure/adapters/http-interactive-demo.adapter.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, map } from 'rxjs'; import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port'; import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model'; import { ApiExecutionResponseDto, ServerTelemetryDto } from '../dtos/interactive-demo.dto'; import { InteractiveDemoMapper } from '../mappers/interactive-demo.mapper'; @Injectable({ providedIn: 'root' }) export class HttpInteractiveDemoAdapter implements InteractiveDemoPort { private readonly http = inject(HttpClient); private readonly baseUrl = '/api/v1/interactive'; getTelemetry(): Observable { const startTime = performance.now(); return this.http.get(`${this.baseUrl}/telemetry`).pipe( map(dto => { const latency = Math.round(performance.now() - startTime); return InteractiveDemoMapper.toDomainTelemetry(dto, latency); }) ); } pingServer(): Observable { return this.http.get(`${this.baseUrl}/ping`).pipe( map(InteractiveDemoMapper.toDomainDemoResult) ); } executeCustomDemo(demoId: string): Observable { return this.http.post(`${this.baseUrl}/execute`, { demo_id: demoId }).pipe( map(InteractiveDemoMapper.toDomainDemoResult) ); } } ``` --- ### 5.3 Capa de Aplicación (`application/stores/`) Administración de estado reactivo mediante **Angular Signals**. ```typescript // application/stores/interactive-api.store.ts import { Injectable, computed, inject, signal } from '@angular/core'; import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port'; import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model'; @Injectable({ providedIn: 'root' }) export class InteractiveApiStore { private readonly demoPort = inject(InteractiveDemoPort); // Signals privadas de estado private readonly _telemetry = signal(null); private readonly _lastResult = signal(null); private readonly _isLoading = signal(false); private readonly _errorMessage = signal(null); // Signals públicas de solo lectura readonly telemetry = this._telemetry.asReadonly(); readonly lastResult = this._lastResult.asReadonly(); readonly isLoading = this._isLoading.asReadonly(); readonly errorMessage = this._errorMessage.asReadonly(); readonly isHealthy = computed(() => this._telemetry()?.status === 'Healthy'); loadTelemetry(): void { this._isLoading.set(true); this._errorMessage.set(null); this.demoPort.getTelemetry().subscribe({ next: (telemetry) => { this._telemetry.set(telemetry); this._isLoading.set(false); }, error: (err) => { this._errorMessage.set(err.message ?? 'No se pudo conectar con el backend .NET'); this._isLoading.set(false); } }); } executePing(): void { this._isLoading.set(true); this._errorMessage.set(null); this.demoPort.pingServer().subscribe({ next: (result) => { this._lastResult.set(result); this._isLoading.set(false); }, error: (err) => { this._errorMessage.set(err.message ?? 'Error al ejecutar ping'); this._isLoading.set(false); } }); } } ``` --- ### 5.4 Capa de Presentación (`presentation/`) #### A. Smart Page Component ```typescript // presentation/features/interactive-api/interactive-api.component.ts import { Component, OnInit, ChangeDetectionStrategy, inject } from '@angular/core'; import { InteractiveApiStore } from '../../../application/stores/interactive-api.store'; import { JsonPrettyPipe } from '../../../shared/pipes/json-pretty.pipe'; @Component({ selector: 'app-interactive-api', standalone: true, imports: [JsonPrettyPipe], templateUrl: './interactive-api.component.html', styleUrl: './interactive-api.component.css', changeDetection: ChangeDetectionStrategy.OnPush }) export class InteractiveApiComponent implements OnInit { protected readonly store = inject(InteractiveApiStore); ngOnInit(): void { this.store.loadTelemetry(); } onTestPing(): void { this.store.executePing(); } } ``` #### B. Template con Control Flow Moderno y Estilo Zen ```html
.NET 8 BACKEND // LIVE TESTER

Interacción en Tiempo Real con la API

@if (store.errorMessage(); as error) { } @if (store.lastResult(); as result) {
GET {{ result.endpoint }} {{ result.httpStatus }} OK {{ result.executionTimeMs }} ms
{{ result.payload | jsonPretty }}
}
``` --- ## 6. Arquitectura de Módulos Específicos ### 6.1 Motor de la Terminal Interactiva (`application/services/terminal-executor.service.ts`) Parser de comandos extensible con mapeo declarativo y llamadas HTTP reales al backend: ```typescript import { Injectable, inject } from '@angular/core'; import { Observable, of, isObservable, map } from 'rxjs'; import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port'; @Injectable({ providedIn: 'root' }) export class TerminalExecutorService { private readonly demoPort = inject(InteractiveDemoPort); private readonly commands: Record Observable | string> = { help: () => `Comandos disponibles: about - Conoce sobre mi perfil y enfoque arquitectónico skills - Stack técnico (.NET, Angular, MySQL, Clean Arch) projects - Listado de proyectos destacados curl - Ejecuta una petición HTTP en vivo al backend .NET retro - Abre el sistema operativo vintage y videojuegos clear - Limpia el buffer de la consola`, about: () => 'Bryan // Fullstack Developer especializado en .NET & Angular Clean Architecture.', skills: () => 'Backend: .NET 8/9, EF Core, MySQL, MediatR, CQRS.\nFrontend: Angular, Signals, OnPush, Canvas 2D.\nPrácticas: Clean Architecture, Ponytail (YAGNI/KISS), TDD.', clear: () => '__CLEAR__', retro: () => 'Abriendo entorno ZenOS...', curl: (args) => { if (args[0] === '/api/v1/interactive/ping' || args[0] === 'ping') { return this.demoPort.pingServer().pipe( map(res => `HTTP ${res.httpStatus} OK (${res.executionTimeMs}ms)\n${JSON.stringify(res.payload, null, 2)}`) ); } return 'Endpoint no reconocido. Prueba con: curl /api/v1/interactive/ping'; } }; execute(rawInput: string): Observable { const parts = rawInput.trim().split(/\s+/); const commandName = parts[0]?.toLowerCase(); const args = parts.slice(1); if (!commandName) return of(''); const handler = this.commands[commandName]; if (!handler) return of(`comando no encontrado: ${commandName}. Escribe "help" para ver la lista.`); const result = handler(args); return isObservable(result) ? result : of(result); } } ``` ### 6.2 Motor del Videojuego Canvas Retro (`retro-game-canvas.component.ts`) Bucle de renderizado nativo en 2D sin librerías pesadas, con limpieza estricta de memoria: ```typescript import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectionStrategy, signal } from '@angular/core'; @Component({ selector: 'app-retro-game-canvas', standalone: true, template: ``, styleUrl: './retro-game-canvas.component.css', changeDetection: ChangeDetectionStrategy.OnPush }) export class RetroGameCanvasComponent implements AfterViewInit, OnDestroy { @ViewChild('gameCanvas') private canvasRef!: ElementRef; private ctx!: CanvasRenderingContext2D; private animationFrameId: number | null = null; private isRunning = false; protected readonly score = signal(0); protected readonly isGameOver = signal(false); ngAfterViewInit(): void { const context = this.canvasRef.nativeElement.getContext('2d'); if (!context) return; this.ctx = context; this.ctx.imageSmoothingEnabled = false; // Pixel-art nítido this.isRunning = true; this.gameLoop(); } private gameLoop = (): void => { if (!this.isRunning) return; this.update(); this.render(); this.animationFrameId = requestAnimationFrame(this.gameLoop); }; private update(): void { // Actualización de entidades y colisiones del juego } private render(): void { this.ctx.fillStyle = '#0a0c10'; this.ctx.fillRect(0, 0, 320, 240); } ngOnDestroy(): void { this.isRunning = false; if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); // Cero memory leaks } } } ``` ### 6.3 Hero 3D con Scroll Desarmable (`hero-3d.component.ts`) Lectura de scroll pasiva y pausa del render loop mediante `IntersectionObserver`: ```typescript import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-hero-3d', standalone: true, templateUrl: './hero-3d.component.html', styleUrl: './hero-3d.component.css', changeDetection: ChangeDetectionStrategy.OnPush }) export class Hero3dComponent implements AfterViewInit, OnDestroy { @ViewChild('canvasContainer') private containerRef!: ElementRef; private isVisible = true; private intersectionObserver?: IntersectionObserver; protected scrollProgress = 0; ngAfterViewInit(): void { this.intersectionObserver = new IntersectionObserver(([entry]) => { this.isVisible = entry.isIntersecting; }, { threshold: 0.05 }); this.intersectionObserver.observe(this.containerRef.nativeElement); window.addEventListener('scroll', this.onScroll, { passive: true }); } private onScroll = (): void => { if (!this.isVisible) return; const el = this.containerRef.nativeElement; const rect = el.getBoundingClientRect(); this.scrollProgress = Math.min(Math.max(-rect.top / window.innerHeight, 0), 1); // Aplicar transformación a las piezas 3D según scrollProgress (0.0 a 1.0) }; ngOnDestroy(): void { window.removeEventListener('scroll', this.onScroll); this.intersectionObserver?.disconnect(); } } ``` --- ## 7. Interceptores y Configuración Global (`app.config.ts`) ```typescript // core/interceptors/auth.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('access_token'); if (token && !req.url.includes('/auth/login')) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } return next(req); }; ``` ```typescript // app.config.ts import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { routes } from './app.routes'; import { authInterceptor } from './core/interceptors/auth.interceptor'; import { errorInterceptor } from './core/interceptors/error.interceptor'; import { InteractiveDemoPort } from './domain/ports/interactive-demo.port'; import { HttpInteractiveDemoAdapter } from './infrastructure/adapters/http-interactive-demo.adapter'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes, withComponentInputBinding()), provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])), // Inversión de dependencias de puertos { provide: InteractiveDemoPort, useClass: HttpInteractiveDemoAdapter } ] }; ``` --- ## 8. Tabla de Anti-Patrones Prohibidos en Frontend | Anti-patrón Prohibido | Solución Obligatoria | |---|---| | Inyectar `HttpClient` directamente en componentes | Inyectar Store/Facade que consuma Ports implementados en `infrastructure/` | | Directivas legadas `*ngIf`, `*ngFor` | Sintaxis moderna `@if`, `@for (item of items; track item.id)` | | `ChangeDetectionStrategy.Default` en componentes | Siempre `ChangeDetectionStrategy.OnPush` | | Bucle infinito en canvas del juego al cambiar de ruta | Limpiar bucle con `cancelAnimationFrame` en `ngOnDestroy()` | | Instalar librerías de 1MB para efectos sencillos | Usar Web APIs nativas: Canvas 2D, Web Audio, IntersectionObserver | | Uso de `any` en respuestas HTTP | Definir DTOs tipados en `infrastructure/dtos/` y mapear a modelos | | Estado global mutable o variables sueltas | Angular Signals con interfaces inmutables (`readonly`) |