Saltar al contenido principal
70 min de lectura1 de agosto de 2026
AngularFrontendTypeScriptComponentesSPAFramework

Requisitos previos

  • typescript

Lo que aprenderás

  • Crear componentes standalone con Angular CLI
  • Usar plantillas con interpolación, @if, @for y @switch
  • Implementar services con inyección de dependencias
  • Consumir APIs con HttpClient
  • Configurar routing con parámetros y lazy loading
Tutorial70 min

Angular: componentes a aplicaciones enterprise

Angular es el framework de Google para construir aplicaciones web completas: componentes, routing, inyección de dependencias y HTTP incluidos de serie. Este tutorial te lleva desde la creación de tu primer proyecto hasta una app con servicios y rutas. Prerequisito conceptual: TypeScript.


¿Qué es Angular?

Angular es un framework completo de frontend mantenido por Google. A diferencia de bibliotecas que solo renderizan vistas, Angular te da todo lo necesario para una aplicación enterprise de fábrica: un sistema de componentes con plantillas, enrutador, formularios, cliente HTTP, inyección de dependencias y herramientas de testing integradas.

  • Framework completo: no necesitas elegir una combinación de bibliotecas; Angular trae las decisiones tomadas.
  • TypeScript nativo: el framework está escrito en TypeScript y lo usa de forma obligatoria.
  • CLI potente: ng generate crea componentes, servicios, módulos y tests con un comando.
  • SPA por diseño: el router carga vistas sin recargar la página.
Desde Angular 17 el modo standalone es la norma: los componentes ya no necesitan NgModules para usarse. Todo lo de este tutorial usa ese enfoque moderno.

Instalar y crear proyecto

Necesitas Node.js LTS instalado. La CLI de Angular se ejecuta con npx, sin instalación global:

npx -p @angular/cli ng new mi-app --standalone

# El CLI pregunta por estilos y SSR. Elige SCSS o CSS plano
# y responde "No" a server-side rendering si quieres una SPA simple.

cd mi-app
ng serve --open   # http://localhost:4200

El flag --standalone genera un proyecto sin NgModules, con componentes independientes. Si lo olvidas, Angular 17+ lo pregunta interactivamente.

La versión de la CLI y la versión del framework deben coincidir. Nunca mezcles ng new de una versión con dependencias de otra en el mismo package.json.

Estructura de un proyecto

Un proyecto standalone generado con el CLI tiene esta anatomía:

mi-app/
├── src/
│   ├── main.ts            # bootstrap de la app
│   ├── app/
│   │   ├── app.config.ts  # providers globales (router, http)
│   │   ├── app.routes.ts  # definición de rutas
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   └── app.component.css
│   ├── index.html
│   └── styles.css
├── angular.json           # config del workspace
├── package.json
└── tsconfig.json

El arranque: main.ts

main.ts arranca la aplicación usando el componente raíz y la configuración global:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

La configuración: app.config.ts

Aquí se registran los providers que toda la app comparte. En un proyecto recién creado ya incluye el router:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
  ],
};

Más adelante añadiremos provideHttpClient() aquí para habilitar peticiones HTTP.

Componentes y plantillas

Un componente es una clase TypeScript decorada con @Component que define el selector, la plantilla (template) y los estilos. En modo standalone declaramos todo en un solo archivo:

import { Component } from '@angular/core';

@Component({
  selector: 'app-saludo',
  standalone: true,
  template: `
    <h2>{{ nombre }}</h2>
    <p>Has hecho clic {{ contador }} veces.</p>
  `,
})
export class SaludoComponent {
  nombre = 'Ana';
  contador = 0;
}

La interpolación {{ }} imprime el valor de una propiedad del componente en el HTML. Angular se encarga de re-renderizar cuando cambia.

Las plantillas modernas usan control flow integrado en lugar de directivas estructurales:

@if (usuario) {
  <p>Bienvenido, {{ usuario.nombre }}.</p>
} @else {
  <p>No hay sesión iniciada.</p>
}

@for (item of items; track item.id) {
  <li>{{ item.nombre }}</li>
} @empty {
  <li>No hay elementos.</li>
}
@if y @for son la sintaxis recomendada desde Angular 17. Las antiguas *ngIf y *ngFor siguen funcionando pero están deprecadas en favor del nuevo control flow. El track de @for es obligatorio y mejora el rendimiento de las listas.

Data binding y eventos

Angular distingue tres direcciones de datos entre la clase y la plantilla:

<!-- Property binding: de la clase al DOM -->
<img [src]="avatarUrl" [alt]="nombre" />

<!-- Event binding: del DOM a la clase -->
<button (click)="incrementar()">Sumar</button>

<!-- Interpolación: lectura simple -->
<p>{{ contador }}</p>
  • [propiedad] asigna un valor de la clase al atributo/elemento HTML.
  • (evento) escucha un evento y llama a un método del componente.
  • {{ }} muestra el valor, siempre en texto.

Para formularios, el two-way binding con ngModel sincroniza el input con la propiedad en ambas direcciones. Requiere importar FormsModule en el componente:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-buscador',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="query" placeholder="Buscar..." />
    <p>Buscando: {{ query }}</p>
  `,
})
export class BuscadorComponent {
  query = '';
}
[(ngModel)] es azúcar sintáctico de [ngModel] + (ngModelChange). Entender esa descomposición te ayuda a escribir tus propios componentes con binding bidireccional.

Directivas

Las directivas modifican el comportamiento o la apariencia de los elementos. Angular trae varias integradas:

<!-- Control flow integrado -->
@if (estaCargando) {
  <p>Cargando...</p>
}

@switch (estado) {
  @case ('ok') { <p>Todo correcto</p> }
  @case ('error') { <p>Algo falló</p> }
  @default { <p>Estado desconocido</p> }
}

@for (tarea of tareas; track tarea.id; let i = $index) {
  <p>{{ i }}. {{ tarea.titulo }}</p>
}

Para estilos condicionales están ngClass y ngStyle:

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-alerta',
  standalone: true,
  imports: [NgClass],
  template: `
    <div [ngClass]="{
      'alerta-exito': tipo === 'exito',
      'alerta-error': tipo === 'error'
    }">
      {{ mensaje }}
    </div>
  `,
})
export class AlertaComponent {
  tipo = 'exito';
  mensaje = 'Guardado correctamente';
}

La regla general: @if/@for/@switch para estructura del DOM, ngClass/ngStyle para clases y estilos, y directivas de atributo personalizadas para comportamiento reutilizable.

Services e inyección de dependencias

Un service es una clase con lógica reutilizable (llamadas HTTP, estado compartido, cálculos). La inyección de dependencias (DI) de Angular crea y entrega esas instancias donde las pidas, sin que tú gestiones el ciclo de vida:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ContadorService {
  private total = 0;

  incrementar(): number {
    this.total += 1;
    return this.total;
  }
}

providedIn: 'root' registra el service en el inyector raíz: es un singleton disponible en toda la app. Para consumirlo dentro de un componente se usa la función inject():

import { Component } from '@angular/core';
import { ContadorService } from './contador.service';

@Component({
  selector: 'app-contador',
  standalone: true,
  template: `<button (click)="sumar()">Total: {{ total }}</button>`,
})
export class ContadorComponent {
  private contadorService = inject(ContadorService);
  total = 0;

  sumar() {
    this.total = this.contadorService.incrementar();
  }
}
inject() es la forma funcional moderna de la inyección, alternativa al constructor clásico constructor(private svc: ContadorService). Se llama solo dentro de contextos de inyección (constructores, initializers de campos o factories de providers).

HttpClient y APIs

Para consumir APIs primero se habilita provideHttpClient() en app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

Después, el service inyecta HttpClient y define métodos tipados que devuelven Observables:

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Usuario {
  id: number;
  nombre: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UsuariosService {
  private http = inject(HttpClient);
  private api = 'https://jsonplaceholder.typicode.com/users';

  listar(): Observable<Usuario[]> {
    return this.http.get<Usuario[]>(this.api);
  }

  obtener(id: number): Observable<Usuario> {
    return this.http.get<Usuario>(`${this.api}/${id}`);
  }

  crear(usuario: Partial<Usuario>): Observable<Usuario> {
    return this.http.post<Usuario>(this.api, usuario);
  }
}

En el componente, asyncPipe se suscribe y desuscribe automáticamente, evitando memory leaks:

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { UsuariosService } from './usuarios.service';

@Component({
  selector: 'app-lista-usuarios',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <ul>
      @for (usuario of usuarios$ | async; track usuario.id) {
        <li>{{ usuario.nombre }}</li>
      }
    </ul>
  `,
})
export class ListaUsuariosComponent {
  private usuariosService = inject(UsuariosService);
  usuarios$ = this.usuariosService.listar();
}
Angular usa Observables de RxJS, no Promesas. Si prefieres Promesas puedes convertir con firstValueFrom(). El asyncPipe es la forma idiomática y evita suscripciones manuales en el componente.

Routing

El router de Angular mapea URLs a componentes. Las rutas se declaran en app.routes.ts:

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UsuarioDetalleComponent } from './usuario-detalle/usuario-detalle.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'usuarios/:id', component: UsuarioDetalleComponent },
  { path: '**', redirectTo: '' },
];

El componente raíz incluye router-outlet (donde se renderiza la ruta activa) y enlaces con routerLink:

import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Inicio</a>
      <a routerLink="/usuarios/1">Usuario 1</a>
    </nav>
    <router-outlet />
  `,
})
export class AppComponent {}

Para leer parámetros de la ruta, habilita withComponentInputBinding() al registrar el router. Así el parámetro llega como input del componente:

import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
  ],
};
import { Component, Input, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { UsuariosService } from './usuarios.service';

@Component({
  selector: 'app-usuario-detalle',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (usuario$ | async; as usuario) {
      <h1>{{ usuario.nombre }}</h1>
      <p>{{ usuario.email }}</p>
    }
  `,
})
export class UsuarioDetalleComponent {
  @Input() id!: string;
  private usuariosService = inject(UsuariosService);
  usuario$ = this.usuariosService.obtener(Number(this.id));
}
Alternativa clásica: inyectar ActivatedRoute y leer snapshot.paramMap.get('id'). withComponentInputBinding() es más declarativo: la ruta pasa el parámetro como si fuera un input y tu componente queda testeable de forma aislada.

Con esto tienes el ciclo completo: componente que renderiza, service que pide datos, router que navega. Angular te da estructura clara a medida que la app crece, y el modo standalone mantiene el código simple sin renunciar a la escala enterprise.


Ejercicios


Angular brilla en proyectos grandes donde la estructura importa: su inyección de dependencias, el router y el modo standalone te dan una base sólida para escalar de un componente a una aplicación enterprise sin perder el control. Empieza con un componente pequeño, añade servicios cuando haya lógica compartida y deja que el router organice el resto.