28 KiB
| name | description |
|---|---|
| portfolio-frontend | 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.
- 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/*, ceroHttpClient). - Los cambios de endpoints en el backend solo afectan a
infrastructure/, jamás a la UI ni al Dominio.
- Las capas internas nunca conocen a las externas. El código fuente solo apunta hacia el centro:
- 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
BehaviorSubjectpara estados locales o UI cuandosignal()es nativo, más limpio y de menor overhead. - RxJS se reserva exclusivamente para flujos asíncronos y cancelables en adaptadores de infraestructura (
HttpClient).
- Todo componente en la capa de presentación debe usar
- 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 medianteoutput(). Cero inyecciones de repositorios o servicios HTTP.
- Sintaxis Moderna de Control de Flujo:
- Uso obligatorio de la nueva sintaxis
@if,@for(contrack),@switchy@defer. - Prohibido el uso de directivas estructurales legadas (
*ngIf,*ngFor).
- Uso obligatorio de la nueva sintaxis
- 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.
- Usar las herramientas nativas de Angular y la Web Platform antes de añadir paquetes
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
@ifo@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.
// 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;
}
// 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<ServerTelemetry>;
abstract pingServer(): Observable<ApiDemoResult>;
abstract executeCustomDemo(demoId: string): Observable<ApiDemoResult>;
}
5.2 Capa de Infraestructura (infrastructure/)
Maneja la comunicación HTTP, DTOs y Mappers hacia el backend .NET.
// 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<T = unknown> {
endpoint: string;
status_code: number;
duration_ms: number;
data: T;
}
// 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
};
}
}
// 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<ServerTelemetry> {
const startTime = performance.now();
return this.http.get<ServerTelemetryDto>(`${this.baseUrl}/telemetry`).pipe(
map(dto => {
const latency = Math.round(performance.now() - startTime);
return InteractiveDemoMapper.toDomainTelemetry(dto, latency);
})
);
}
pingServer(): Observable<ApiDemoResult> {
return this.http.get<ApiExecutionResponseDto>(`${this.baseUrl}/ping`).pipe(
map(InteractiveDemoMapper.toDomainDemoResult)
);
}
executeCustomDemo(demoId: string): Observable<ApiDemoResult> {
return this.http.post<ApiExecutionResponseDto>(`${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.
// 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<ServerTelemetry | null>(null);
private readonly _lastResult = signal<ApiDemoResult | null>(null);
private readonly _isLoading = signal<boolean>(false);
private readonly _errorMessage = signal<string | null>(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
// 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
<!-- presentation/features/interactive-api/interactive-api.component.html -->
<section class="zen-card api-tester-container">
<header class="card-header">
<div class="header-badge">
<span class="status-dot" [class.online]="store.isHealthy()"></span>
<span class="mono-label">.NET 8 BACKEND // LIVE TESTER</span>
</div>
<h2 class="section-title">Interacción en Tiempo Real con la API</h2>
</header>
<div class="control-actions">
<button
type="button"
class="zen-btn-primary"
[disabled]="store.isLoading()"
(click)="onTestPing()"
>
@if (store.isLoading()) {
<span>Consultando...</span>
} @else {
<span>Ejecutar Ping Controlado</span>
}
</button>
</div>
@if (store.errorMessage(); as error) {
<div class="zen-alert-error" role="alert">
<span>{{ error }}</span>
</div>
}
@if (store.lastResult(); as result) {
<div class="api-response-viewer">
<div class="response-meta">
<span class="method-tag">GET</span>
<span class="url-tag">{{ result.endpoint }}</span>
<span class="status-tag" [class.success]="result.isSuccess">
{{ result.httpStatus }} OK
</span>
<span class="latency-tag">{{ result.executionTimeMs }} ms</span>
</div>
<pre class="json-code"><code>{{ result.payload | jsonPretty }}</code></pre>
</div>
}
</section>
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:
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<string, (args: string[]) => Observable<string> | 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 <url> - 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<string> {
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:
import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-retro-game-canvas',
standalone: true,
template: `<canvas #gameCanvas width="320" height="240" class="pixel-canvas"></canvas>`,
styleUrl: './retro-game-canvas.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RetroGameCanvasComponent implements AfterViewInit, OnDestroy {
@ViewChild('gameCanvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D;
private animationFrameId: number | null = null;
private isRunning = false;
protected readonly score = signal<number>(0);
protected readonly isGameOver = signal<boolean>(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:
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<HTMLDivElement>;
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)
// 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);
};
// 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) |