Entrada

Arquitectura de componentes

Angular: Arquitectura de componentes

Angular es un framework frontend completo, orientado a aplicaciones de gran escala, que se apoya fuertemente en una arquitectura bien definida. A partir de Angular 16 y consolidado en Angular 17, 18, 19 y versiones posteriores, la arquitectura ha evolucionado para ser más modular, reactiva y flexible, manteniendo compatibilidad con proyectos tradicionales.

Este artículo analizaremos la arquitectura moderna de Angular, cómo se organizan sus piezas principales y qué cambios conceptuales debes entender si trabajas con versiones recientes.

Visión general de la arquitectura

Angular usa una arquitectura basada en componentes, con separación por responsabilidad. La arquitectura se basa en los siguientes pilares:

Arquitectura

Todo gira en torno a componentes reactivos, conectados por servicios y orquestados por el router.

Componentes: el núcleo de Angular

Un componente es la unidad fundamental de Angular. Define:

Una clase (lógica). Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.scss'],
})
export class UserCardComponent {
  @Input() name!: string;
  @Input() role!: string;
  @Input() isActive: boolean = true;

  toggleStatus(): void {
    this.isActive = !this.isActive;
  }
}

El archivo muestra:

  • Clase del componente (lógica)
  • standalone: true (NO es estrictamente necesario desde Angular 17+)
  • Decorador @Component
    • Metadatos el objeto del decorador
  • Uso de @Input() para comunicación

El símbolo !, por ejemplo @Input() name!: string; es el Non-Null Assertion Operator de TypeScript. Indica al compilador que la propiedad será inicializada más adelante (por Angular), aunque no tenga un valor en el constructor, evitando errores de compilación por variables no asignadas.

Un template HTML (vista). Por ejemplo:

1
2
3
4
5
6
7
8
<div class="user-card" [class.inactive]="!isActive">
  <h3>{{ name }}</h3>
  <p>{{ role }}</p>

  <button (click)="toggleStatus()">
    {{ isActive ? 'Desactivar' : 'Activar' }}
  </button>
</div>

El archivo muestra:

  • Interpolación {{ }}
  • Bindig de clases
  • Manejo de eventos (click)

Estilos (CSS/SCSS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.user-card {
  border: 1px solid #ddd;
  padding: 1rem;
  border-radius: 6px;
  max-width: 250px;

  h3 {
    margin: 0 0 0.5rem;
  }

  button {
    margin-top: 0.5rem;
    cursor: pointer;
  }

  &.inactive {
    opacity: 0.5;
  }
}

EL archivo muestra:

  • Estilos encapsulados al componente
  • Uso de SCSS

En cualquier otro componente standalone podemos reutilizar el componente anterior sin necesidad de declararlo en un módulo. Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { UserCardComponent } from './components/user-card/user-card.component';

@Component({
  standalone: true,
  imports: [UserCardComponent],
  template: `
    <app-user-card
      name="Marco Contreras"
      role="Full Stack Developer | Cloud Architect "
    />
  `,
})
export class HomeComponent {}

De lo contrario, utilizando el enfoque tradicional basado en NgModule, el componente no es standalone y las dependencias deben declararse en un módulo. Por ejemplo, para habilitar two-way binding con ngModel, es necesario importar FormsModule en el módulo principal:

1
2
3
4
5
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
})
export class AppModule {}

Luego, el componente puede utilizar los distintos tipos de binding sin declarar dependencias propias:

1
2
<!-- app.component.html -->
<input [(ngModel)]="username" />

En este enfoque, la configuración se centraliza en el módulo, a diferencia del modelo standalone, donde cada componente declara explícitamente lo que necesita.

Componentes Standalone

Como ya hemos mencionado, los componentes independientes, también conocidos como “standalone components” en inglés, son unidades independientes que no requieren ser declarados en un NgModule. Introducidos para simplificar el desarrollo (estable desde v15, predeterminado en v17+), gestionan sus propias dependencias importando módulos, directivas o componentes directamente en el decorador.

Características:

  • Independencia: No requieren ser declarado en un módulo padre.
  • Importaciones directas: El componente importa sus propias dependencias (imports: [...]).
  • Menos código: Se elimina la necesidad de crear un NgModule por cada componente, simplificando la estructura.
  • Reutilización: Son más fáciles de mover y usar en diferentes partes de la aplicación.
  • Rendimiento: Mejoran el “tree-shaking”, permitiendo a Angular eliminar código no utilizado de manera más eficiente.

Generar componentes standalone con Angular CLI

Crear estos componentes en versiones superiores a v14, es un proceso bien simple. Puedes usar Angular CLI o crear componentes manualmente en el decarador de componentes de Angular.

Ejemplo, para crear un componente usando Angular CLI:

1
ng generate component "nombre-componente" --standalone

En Angular 17 (y superiores: 18, 19, 20…) standalone es el comportamiento por defecto. El CLI asume que todo es standalone y basta con:

1
ng generate component "nombre-componente"

Ejemplo conceptual:

  • Un componente importa otros componentes, pipes o directivas directamente
  • El árbol de dependencias es explícito

Ejemplo de componente:

1
2
3
4
5
6
7
8
@Component({
  selector: 'app-user-card',
  imports: [CommonModule],
  templateUrl: './user-card.component.html'
})
export class UserCardComponent {
  @Input() user!: User;
}

Templates y sistema de renderizado

Los templates en Angular:

  • Usan HTML extendido
  • Permiten data binding
  • Soportan directivas estructurales y de atributo

Tipos de binding

  • Interpolación:
  • Property binding
  • Event binding
  • Two-way binding

Interpolación

Se utiliza para mostrar datos desde la clase hacia la vista.

1
2
<h2>Hola {{ username }}</h2>
<p>Edad: {{ age }}</p>

Componente .ts

1
2
username = 'Marco';
age = 28;

Nuevo control de flujo

Desde Angular 17 se incorporan nuevas estructuras de control basadas en sintaxis declarativa, entre las que destacan:

  • @if
  • @for
  • @switch

Estas nuevas directivas reemplazan progresivamente a *ngIf, *ngFor y *ngSwitch, alineándose con la arquitectura moderna de Angular y los componentes standalone.

Antes, con la sintaxis tradicional:

1
2
3
<div *ngIf="user">
  <p>{{ user.name }}</p>
</div>

Ahora, usando la nueva sintaxis:

1
2
3
@if (user) {
  <p>{{ user.name }}</p>
}

Y para listas:

1
2
3
@for (item of items; track item.id) {
  <li>{{ item.name }}</li>
}

Este enfoque hace que las plantillas sean más fáciles de leer, mantener y optimizar, acercando Angular a una experiencia más declarativa y predecible.


Servicios y lógica de negocio

Los servicios contienen:

  • Lógica reutilizable
  • Acceso a APIs
  • Gestión de estado
  • Utilidades

No tienen vista y se usan mediante inyección de dependencias.

Buenas prácticas:

  • Un servicio = una responsabilidad
  • Evitar lógica compleja en componentes
  • Usar servicios para comunicación entre componentes

Inyección de dependencias (DI)

Angular cuenta con un sistema de inyección de dependencias jerárquico y altamente optimizado, que permite administrar y compartir servicios de forma eficiente a lo largo de la aplicación.

Este sistema se basa en un árbol de inyectores, donde cada nivel (aplicación, rutas y componentes) puede definir sus propios providers, heredando o sobrescribiendo dependencias según sea necesario.

Características principales

  • Providers a distintos niveles: global, componente o ruta
  • Árbol de inyectores, que define el alcance y reutilización de las instancias
  • Gestión automática de instancias, sin necesidad de crear o destruir servicios manualmente

Enfoque en versiones modernas de Angular

En las versiones más recientes de Angular se promueve un uso más explícito y controlado de la DI, priorizando:

  • Providers definidos directamente en componentes y rutas, en lugar de centralizarlos solo en módulos
  • Uso de la función inject(), evitando constructores innecesarios cuando el servicio se utiliza de forma puntual

Ejemplo con inject()

1
2
3
4
5
6
7
8
9
10
11
12
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
  standalone: true,
  selector: 'app-user',
  template: `{{ userService.getUserName() }}`,
  providers: [UserService]
})
export class UserComponent {
  private userService = inject(UserService);
}

En este ejemplo, el servicio queda encapsulado dentro del componente, asegurando que su ciclo de vida esté directamente ligado a él.

Beneficios de este enfoque

  • Mayor control del ciclo de vida de las dependencias
  • Mejor encapsulación y aislamiento entre componentes
  • Código más claro y alineado con la arquitectura standalone

Ruteo y arquitectura de navegación

El router de Angular permite:

  • Navegación por vistas
  • Lazy loading
  • Guards
  • Resolvers

Lazy loading moderno

En Angular actual:

  • Se cargan componentes standalone directamente
  • No se requieren módulos intermedios

Esto reduce el tamaño del bundle inicial y mejora el rendimiento.

Guards y Resolvers

Se usan para:

  • Controlar acceso
  • Preparar datos antes de renderizar
  • Proteger rutas

Estado y reactividad

Angular siempre ha sido reactivo, pero desde Angular 16 en adelante esto se refuerza con:

Signals

Los signals introducen un modelo reactivo nativo:

  • Estado explícito
  • Actualizaciones automáticas
  • Menos dependencia de RxJS para casos simples

Ventajas:

  • Código más claro
  • Menos suscripciones manuales
  • Mejor rendimiento

RxJS sigue siendo clave para flujos complejos y asincronía avanzada.

NgModules vs arquitectura moderna

NgModules (arquitectura clásica)

  • Organización por módulos
  • Declaraciones, imports y providers
  • Mayor boilerplate

Standalone (arquitectura actual)

  • Componentes autosuficientes
  • Imports explícitos
  • Menor complejidad
  • Mejor tree-shaking

NgModules no están obsoletos, pero ya no son el enfoque recomendado para nuevos proyectos.


Compilación, build y rendimiento

Angular utiliza:

  • Compilación AOT por defecto
  • Tree-shaking
  • Optimización de bundles

En producción:

  • Se eliminan comprobaciones de desarrollo
  • Se minimiza el código
  • Se optimizan assets

Esto se gestiona mediante configuraciones de build definidas en angular.json.

Organización recomendada del proyecto

Arquitectura típica moderna:

  • core/

    • servicios globales
    • interceptores
  • shared/

    • componentes reutilizables
    • pipes
  • features/

    • funcionalidades independientes
    • componentes standalone
  • app.routes.ts
  • app.config.ts

Este enfoque favorece escalabilidad y mantenimiento.

Ejemplos de servicios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class ProductService {

  http = inject(HttpClient);

  getAllProducts(){
    return this.http.get("https://api.freeprojectapi.com/api/Ecommerce/get-products")
  }
}
Esta entrada está licenciada bajo CC BY 4.0 por el autor.