Módulo 00: Introducción a Angular

Objetivos del módulo

  • Entender qué es Angular, qué problema resuelve y en qué se diferencia una plataforma completa de una librería de interfaces
  • Leer un componente real de principio a fin, aunque todavía no sepas escribirlo
  • Saber qué ocurre desde que el navegador abre index.html hasta que aparece la aplicación, y qué hace el compilador con tus plantillas
  • Conocer la historia de Angular: AngularJS, la reescritura de 2016 y el renacimiento con signals
  • Comparar Angular con React y Vue con criterios concretos, sin discursos de fans
  • Interpretar la política de versiones: versionado semántico, ciclo anual, soporte activo y LTS, estados developer preview y experimental
  • Decidir qué versión usar, y saber exactamente qué pierdes si eliges la LTS
  • Preparar lo necesario para seguir el curso y arrancar la API de ejemplo EMPRESA
  • Orientarte en el mapa del curso y leer las marcas 🆕 y 🔄

1. Qué es Angular

Angular es una plataforma de código abierto, mantenida por Google, para construir aplicaciones web con TypeScript. La palabra importante es plataforma. Angular no te da solo una forma de pintar la interfaz: te da también el enrutado, los formularios, el cliente HTTP, la inyección de dependencias, las pruebas, el compilador y una herramienta de línea de órdenes que los une.

Para entender qué aporta, piensa en lo que hace una aplicación como un panel de gestión de EMPRESA, la aplicación que construiremos a lo largo del curso:

  • Muestra listas de empleados y departamentos que llegan de una API.
  • Tiene varias “pantallas” (listado, ficha, edición), pero el navegador nunca recarga la página.
  • Valida formularios mientras escribes.
  • Recuerda quién ha iniciado sesión y protege ciertas rutas.
  • Cuando cambia un dato, solo se actualiza la parte de la pantalla que depende de él.

Todo eso se puede hacer con JavaScript puro. De hecho, si has hecho el Curso de JavaScript sabrías empezar: un fetch, un document.createElement, un addEventListener… El problema aparece cuando el programa crece. Mantener a mano la coherencia entre los datos y el DOM es la mayor fuente de errores de las interfaces: hay que recordar actualizar cada sitio donde se muestra un dato, y basta olvidar uno para que la pantalla mienta.

Aplicaciones de una sola página

Una aplicación Angular normal es una aplicación de una sola página (single-page application, SPA). El servidor entrega un HTML casi vacío y un paquete de JavaScript. A partir de ahí, es el JavaScript quien construye la interfaz en el navegador, cambia de “pantalla” sin recargar y pide al servidor solo datos, normalmente en JSON.

  Web tradicional SPA
Cambiar de página El servidor genera y envía HTML nuevo El navegador cambia la vista sin recargar
Qué viaja por la red HTML completo Datos JSON
Dónde vive el estado de la interfaz En el servidor y en la URL En la memoria del navegador
Primera carga Rápida Más lenta: hay que descargar y ejecutar el JavaScript
Posicionamiento en buscadores Directo Necesita ayuda, por ejemplo renderizado en servidor

Los dos inconvenientes de la última parte de la tabla tienen solución dentro del propio Angular: el renderizado en servidor y la hidratación, que verás en el módulo 24.

Plataforma frente a librería

En una librería tu programa manda: la llamas cuando la necesitas. En un framework el control se invierte: tú escribes piezas con la forma que él pide, y él decide cuándo crearlas, cuándo pintarlas y cuándo destruirlas. Por eso se dice que un framework te llama a ti.

Angular es de los segundos, y además es de los que más traen de serie. Es como comprar una cocina equipada frente a comprar solo la encimera: con la cocina equipada tienes menos elección, pero todo encaja y se actualiza a la vez.

Capas de Angular: la aplicación arriba, los paquetes oficiales como core, router, forms y http en el medio, y la CLI y el compilador debajo

Figura 1. Lo que trae Angular de serie. Todos los paquetes oficiales avanzan juntos en la misma versión mayor.

Necesidad En Angular Módulo del curso
Componentes y plantillas @angular/core 03, 04
Estado reactivo Señales de @angular/core 05
Compartir lógica entre componentes Servicios e inyección de dependencias 08
Navegación @angular/router 10, 11
Peticiones HTTP HttpClient, httpResource 12, 13
Formularios @angular/forms y Signal Forms 16 a 18
Componentes visuales @angular/material y @angular/cdk 20
Accesibilidad @angular/aria 21
Render en servidor @angular/ssr 24
Pruebas Vitest y TestBed 26
Crear, compilar, actualizar Angular CLI, la orden ng 01

📘 Concepto: Que Angular sea una plataforma “con pilas incluidas” tiene una consecuencia práctica: dos proyectos Angular de empresas distintas se parecen mucho entre sí. Si cambias de equipo, las rutas, los formularios y las peticiones se hacen igual. Es uno de los motivos por los que se usa tanto en aplicaciones empresariales grandes, con muchos programadores y una vida larga.

Las piezas básicas de una aplicación

Antes de ver código, conviene poner nombre a las piezas. Todas se estudian a fondo más adelante; aquí solo necesitas la idea.

Pieza Qué es Comparación
Componente Una clase con su plantilla HTML y sus estilos. Es un trozo reutilizable de interfaz Una pieza de LEGO con forma propia
Plantilla HTML ampliado con enlaces a los datos y control de flujo (@if, @for) El molde de la pieza
Señal (signal) Un valor que avisa a quien lo lee cuando cambia Un cartel de precios electrónico: si cambia el dato central, se actualiza solo
Servicio Una clase con lógica o datos compartidos, sin interfaz El almacén al que acuden todas las cocinas
Inyección de dependencias El mecanismo que entrega a cada clase lo que pide El repartidor del almacén
Ruta La asociación entre una URL y un componente La carta de un restaurante con su número de plato

2. Un primer vistazo

La mejor forma de saber de qué va algo es verlo funcionar. Este es un programa completo: una lista con los siete departamentos reales de EMPRESA, un cuadro para filtrarlos por localidad y el presupuesto total de la empresa. No te preocupes si no entiendes todos los detalles: el objetivo es que reconozcas la forma de una aplicación Angular moderna. Cada línea se explica en módulos posteriores.

Aplicación con un título, un cuadro de filtro y la lista de los siete departamentos de EMPRESA con sus localidades

Figura 2. La aplicación del ejemplo recién cargada.

💡 Consejo: No hace falta que lo escribas ahora. Si quieres probarlo cuando termines el módulo 01, crea un proyecto con npx @angular/cli new empresa y sustituye los ficheros de src/ por los de este apartado.

El servicio Departamentos con @Service 🆕 v22.0

🆕 Desde Angular 22.0. No existe en la LTS (Angular 21). Si usas la LTS: importa Injectable y escribe @Injectable({ providedIn: 'root' }) en lugar de @Service(); el resto del ejemplo compila igual.

Un servicio guarda los datos y la lógica que no pertenecen a ninguna pantalla en concreto. Fichero src/app/departamentos.ts:

import { Service, computed, signal } from '@angular/core';

export interface Departamento {
  deptId: number;
  nombre: string;
  localidad: string;
  presupuesto: number | null;
}

@Service()
export class Departamentos {
  private readonly lista = signal<Departamento[]>([
    { deptId: 10, nombre: 'DIRECCIÓN', localidad: 'Madrid', presupuesto: 500000 },
    { deptId: 20, nombre: 'VENTAS', localidad: 'Madrid', presupuesto: 350000 },
    { deptId: 30, nombre: 'INFORMÁTICA', localidad: 'Valencia', presupuesto: 420000 },
    { deptId: 40, nombre: 'CONTABILIDAD', localidad: 'Sevilla', presupuesto: 150000 },
    { deptId: 50, nombre: 'MARKETING', localidad: 'Barcelona', presupuesto: 200000 },
    { deptId: 60, nombre: 'LOGÍSTICA', localidad: 'Zaragoza', presupuesto: 180000 },
    { deptId: 70, nombre: 'INVESTIGACIÓN', localidad: 'Bilbao', presupuesto: null },
  ]);

  readonly todos = this.lista.asReadonly();

  readonly presupuestoTotal = computed(() =>
    this.lista().reduce((suma, d) => suma + (d.presupuesto ?? 0), 0),
  );
}

Fíjate en cuatro cosas:

  • signal(...) crea un valor reactivo. Se lee llamándolo como una función: this.lista().
  • computed(...) crea un valor derivado. Nadie tiene que acordarse de recalcular el total: Angular sabe que depende de lista y lo recalcula solo cuando lista cambia.
  • asReadonly() publica la lista en modo de solo lectura. Los componentes pueden leerla, pero no modificarla por su cuenta.
  • @Service() es un decorador: una marca que dice a Angular que esta clase se puede pedir desde otras clases, y que debe haber una sola instancia en toda la aplicación.

Si los decoradores, las interfaces o number | null te resultan extraños, el módulo 02 cubre el TypeScript que necesitas.

El componente, sin changeDetection 🔄 v22.0

🔄 Cambia en Angular 22.0. Un componente sin changeDetection usa OnPush; en la LTS (Angular 21) el valor por defecto es Default, que Angular 21.2 también llama Eager. Si usas la LTS: añade changeDetection: ChangeDetectionStrategy.OnPush a cada componente para tener el mismo comportamiento.

Fichero src/app/app.ts:

import { CurrencyPipe } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { Departamentos } from './departamentos';

@Component({
  selector: 'app-root',
  imports: [CurrencyPipe],
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {
  private readonly departamentos = inject(Departamentos);

  protected readonly filtro = signal('');
  protected readonly total = this.departamentos.presupuestoTotal;

  protected readonly visibles = computed(() => {
    const texto = this.filtro().trim().toLowerCase();
    return this.departamentos
      .todos()
      .filter((d) => d.localidad.toLowerCase().includes(texto));
  });

  protected alEscribir(evento: Event) {
    this.filtro.set((evento.target as HTMLInputElement).value);
  }
}
  • @Component convierte la clase en un componente. selector es la etiqueta HTML que lo representa, imports lista lo que usa su plantilla, y templateUrl y styleUrl apuntan a sus ficheros.
  • inject(Departamentos) pide el servicio. El componente no hace new: lo pide y Angular se lo entrega.
  • filtro es el texto que ha escrito el usuario, en una señal.
  • visibles depende de dos señales, filtro y la lista del servicio. Si cualquiera de las dos cambia, se recalcula. Ese es el corazón del modelo: describes cómo se obtiene un dato y Angular se encarga de cuándo.
  • protected deja el miembro accesible para la plantilla, pero no para otras clases.

Y fíjate también en lo que no hay: no hay ningún NgModule, ni standalone: true, ni ngOnInit, ni sufijo .component en el fichero. Si encuentras un tutorial que los usa, es de otra época, como veremos en la sección 4.

La plantilla

Fichero src/app/app.html:

<main>
  <h1>Departamentos de EMPRESA</h1>

  <label>
    Filtrar por localidad
    <input type="search" placeholder="Madrid" (input)="alEscribir($event)" />
  </label>

  <p class="resumen">
    {{ visibles().length }} de 7 departamentos.
    Presupuesto total de la empresa: {{ total() | currency: 'EUR' : 'symbol' : '1.0-0' : 'es' }}
  </p>

  @if (visibles().length > 0) {
    <ul>
      @for (d of visibles(); track d.deptId) {
        <li>
          <strong>{{ d.nombre }}</strong>
          <span>{{ d.localidad }}</span>
        </li>
      }
    </ul>
  } @else {
    <p class="vacio">Ningún departamento en «{{ filtro() }}».</p>
  }
</main>
Sintaxis Qué significa Módulo
{{ expresión }} Interpolación: escribe el valor como texto 04
(input)="..." Escucha un evento del DOM 04
\| currency: ... Pipe: transforma un valor para mostrarlo 07
@if / @else Control de flujo condicional 04
@for (...; track ...) Repite un bloque por cada elemento 04

La plantilla es HTML con añadidos, no JavaScript. Esta es una decisión de diseño muy característica de Angular: un diseñador que sepa HTML puede leerla, y el compilador puede analizarla y optimizarla porque su sintaxis está acotada.

Ver los ficheros app.css y main.ts

src/app/app.css:

main {
  max-width: 560px;
  margin: 2rem auto;
  font-family: 'Segoe UI', Arial, sans-serif;
  color: #37474f;
}

h1 {
  color: #1a237e;
  font-size: 1.6rem;
}

label {
  display: grid;
  gap: 0.25rem;
  font-weight: 600;
}

input {
  padding: 0.5rem;
  border: 1px solid #3f51b5;
  border-radius: 6px;
  font: inherit;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.25rem;
  background: #e8eaf6;
  border-radius: 6px;
}

.vacio {
  color: #d84315;
}

src/main.ts, con el registro del formato español que usa el pipe currency (lo explica el módulo 07):

import { registerLocaleData } from '@angular/common';
import es from '@angular/common/locales/es';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';

registerLocaleData(es);

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

Qué pasa al escribir en el filtro

Si escribes madrid, ocurre esto, sin que hayas tocado el DOM ni una vez:

  1. El evento input llama a alEscribir, que hace filtro.set('madrid').
  2. La señal filtro avisa de que ha cambiado. visibles queda marcada como pendiente de recalcular.
  3. Angular revisa la vista que lee visibles. Al hacerlo, computed ejecuta el filtro y devuelve dos departamentos.
  4. @for compara la lista nueva con la anterior usando track d.deptId: conserva los nodos de DIRECCIÓN y VENTAS y elimina los otros cinco.
  5. La interpolación actualiza “2 de 7 departamentos”. El total no cambia, porque presupuestoTotal no depende de filtro.

La misma aplicación con madrid escrito en el filtro, mostrando solo Dirección y Ventas y el texto 2 de 7 departamentos

Figura 3. Tras escribir madrid quedan dos departamentos. El presupuesto total, 1.800.000 €, sigue siendo el de toda la empresa.

Y si ningún departamento coincide, la rama @else sustituye a la lista:

La aplicación con cuenca en el filtro y el mensaje Ningún departamento en cuenca

Figura 4. La rama @else, con el texto real que produce la plantilla.

💡 Consejo: Observa que en el presupuesto total INVESTIGACIÓN cuenta como 0, porque su presupuesto es null y el operador ?? lo sustituye. Es el dato real de la API: un buen ejemplo de por qué conviene que el tipo number | null te obligue a pensar en ese caso.

Pedir los datos a la API con httpResource 🔄 v22.0

🔄 Cambia en Angular 22.0. httpResource pasa a ser estable; en la LTS (Angular 21) existe, pero es experimental y puede cambiar sin aviso. Si usas la LTS: este ejemplo compila igual, solo que asumes ese riesgo; si prefieres una API estable, usa HttpClient con toSignal, como se explica en el módulo 15.

Los datos escritos a mano son útiles para empezar, pero EMPRESA tiene una API real (sección 9). Este componente pide los departamentos y muestra los tres estados posibles: cargando, error y datos.

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

interface Pagina<T> {
  contenido: T[];
  total: number;
}

interface Departamento {
  deptId: number;
  nombre: string;
  localidad: string;
}

@Component({
  selector: 'app-root',
  template: `
    <main>
      <h1>Departamentos desde la API</h1>

      @if (respuesta.isLoading()) {
        <p>Cargando...</p>
      } @else if (respuesta.error()) {
        <p class="vacio">No se ha podido contactar con la API. ¿Está arrancada?</p>
      } @else if (respuesta.hasValue()) {
        <p>{{ respuesta.value().total }} departamentos</p>
        <ul>
          @for (d of respuesta.value().contenido; track d.deptId) {
            <li>
              <strong>{{ d.nombre }}</strong>
              <span>{{ d.localidad }}</span>
            </li>
          }
        </ul>
      }
    </main>
  `,
  styleUrl: './app.css',
})
export class DepartamentosApi {
  protected readonly respuesta = httpResource<Pagina<Departamento>>(
    () => 'http://localhost:3000/api/departamentos',
  );
}

La idea es la misma de antes: respuesta es un objeto hecho de señales (isLoading, error, value), así que la plantilla reacciona sola según llegan los datos. No hay suscripciones que cancelar ni variables de estado que mantener a mano. Fíjate también en que no ha hecho falta tocar app.config.ts: HttpClient está disponible sin configurar nada, tanto en Angular 22 como en la LTS, y provideHttpClient() solo se añade para personalizarlo. Lo estudiarás a fondo en los módulos 12 y 13.

Lista de los siete departamentos obtenida de la API con el texto 7 departamentos

Figura 5. Los datos reales de la API de EMPRESA.

Mensaje en rojo que indica que no se ha podido contactar con la API

Figura 6. El mismo componente con la API apagada.


3. Qué ocurre por dentro

Has visto qué escribes. Ahora toca ver qué hace Angular con ello. Entender estos dos procesos, el arranque y la compilación, te evitará muchas confusiones más adelante.

El arranque de la aplicación

El navegador solo entiende HTML, CSS y JavaScript. Lo primero que recibe es un index.html casi vacío:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Empresa</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

La única pista de que hay una aplicación es esa etiqueta <app-root>, que el navegador no conoce. Al compilar, la CLI añade los <script> y los estilos. Después:

Cinco pasos del arranque: index.html, main.ts, app.config.ts, el componente App y el ciclo reactivo

Figura 7. Del HTML vacío a la aplicación en marcha.

El código que inicia todo es main.ts, y app.config.ts declara los servicios que toda la aplicación necesita. Este es el que genera ng new en un proyecto recién creado:

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

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

El resultado es un árbol: App usa otros componentes, que usan otros, y Angular los va creando y colocando dentro de <app-root>.

⚠️ Importante: Los componentes ni se recargan ni “se navega” a ellos como a páginas. Cambiar de ruta destruye unos componentes y crea otros dentro de la misma página. Por eso una variable global de window sobrevive a la navegación, y por eso una recarga del navegador (F5) pierde todo lo que estaba en memoria.

El compilador de plantillas

Una plantilla no llega al navegador tal como la escribes. El compilador de Angular la convierte en JavaScript antes de publicar la aplicación, durante ng build o ng serve. A esto se le llama compilación anticipada (ahead-of-time, AOT). Este es un fragmento real del main.js que genera ng build --configuration development para la plantilla anterior:

static ɵcmp = ɵɵdefineComponent({ type: _App, selectors: [["app-root"]], ...
  template: function App_Template(rf, ctx) {
    if (rf & 1) {
      ɵɵdomElementStart(0, "main")(1, "h1");
      ɵɵtext(2, "Departamentos de EMPRESA");
      ɵɵdomElementEnd();
      ɵɵdomElementStart(3, "label");
      ɵɵtext(4, " Filtrar por localidad ");
      ɵɵdomElementStart(5, "input", 0);
      ɵɵdomListener("input", function App_Template_input_input_5_listener($event) {
        return ctx.alEscribir($event);
      });
      ...
      ɵɵconditionalCreate(9, App_Conditional_9_Template, 3, 0, "ul")(10, ...
    }
    if (rf & 2) {
      ɵɵadvance(7);
      ɵɵtextInterpolate2(" ", ctx.visibles().length, " de 7 departamentos. ...
      ɵɵadvance(2);
      ɵɵconditional(ctx.visibles().length > 0 ? 9 : 10);
    }

No tienes que escribir ni memorizar nada de esto: las funciones con ɵ son internas y cambian entre versiones. Pero te enseña tres cosas:

  • La plantilla es código. Cada elemento es una instrucción. Por eso un error en la plantilla es un error de compilación, no un fallo silencioso en el navegador.
  • Hay dos pasadas separadas. El bloque rf & 1 crea el DOM una sola vez. El bloque rf & 2 actualiza únicamente lo que puede cambiar: el título h1 no aparece ahí porque es texto fijo.
  • Se comprueban los tipos. El compilador sabe que ctx.visibles() devuelve Departamento[]. Si escribes d.nombr en la plantilla, la compilación falla con un error que señala esa línea.

📘 Concepto: Esta es una diferencia profunda con otras herramientas. En React, una función de componente se vuelve a ejecutar y produce una descripción nueva de la interfaz, que se compara con la anterior. Angular, en cambio, ha separado de antemano la parte fija de la parte dinámica, y solo visita las expresiones dinámicas de las vistas que hay que revisar.

Cómo sabe Angular que algo ha cambiado

Esta pregunta define la historia reciente de Angular. Si un dato cambia, alguien tiene que avisar para actualizar el DOM. Durante años, la respuesta fue una librería llamada Zone.js, que modificaba las funciones asíncronas del navegador (setTimeout, addEventListener, las promesas…) para enterarse de que había terminado una tarea. Como Zone.js no sabía qué había cambiado, Angular revisaba todo el árbol de componentes por si acaso.

Las señales cambian el planteamiento: el dato sabe quién lo lee, y avisa directamente.

A la izquierda, Zone.js intercepta cualquier tarea y Angular revisa todo el árbol. A la derecha, una señal marca solo las vistas que la leen

Figura 8. Antes había que adivinar y revisar todo. Con señales, se actualiza solo lo afectado.

  Con Zone.js Zoneless con señales
Quién dispara la actualización Cualquier tarea asíncrona Un cambio de señal, entre otros avisos concretos
Qué se revisa Todo el árbol, salvo ramas OnPush Las vistas marcadas
Coste en la descarga La librería Zone.js Ninguno
Depuración Pilas de llamadas llenas de código de Zone.js Pilas de llamadas limpias
Modificar un campo normal Se ve solo, casi siempre Hay que avisar: mejor usa señales

Un proyecto nuevo ya no incluye Zone.js: es lo que se llama zoneless. El módulo 09 cuenta este mecanismo con detalle. Y la última fila es la que debes recordar desde hoy: si un dato se muestra en pantalla y puede cambiar, guárdalo en una señal.


4. Historia de Angular

La historia importa por un motivo práctico: en Internet convive material de épocas muy distintas, y necesitas reconocer a cuál pertenece lo que estás leyendo.

Línea de tiempo de 2010 a 2026 con tres etapas: AngularJS, Angular con TypeScript y NgModule, y el renacimiento con standalone, señales y zoneless

Figura 9. Tres etapas de Angular.

AngularJS (2010-2021)

Miško Hevery creó AngularJS, cuya primera versión se publicó el 20 de octubre de 2010. En aquella época, una interfaz dinámica significaba jQuery y mucho código manual sobre el DOM. AngularJS aportó ideas que entonces eran revolucionarias: enlace de datos bidireccional (cambias el modelo y cambia la vista), directivas que ampliaban el HTML e inyección de dependencias en el navegador.

Su mecanismo tenía un límite: para detectar cambios comparaba todos los datos enlazados en cada ciclo (dirty checking), lo que se volvía lento en páginas grandes. Además estaba escrito para el JavaScript anterior a ES2015, sin módulos ni clases. La versión 1.7 recibió soporte a largo plazo hasta el 31 de diciembre de 2021. Hoy es un producto retirado: no recibe parches de seguridad.

Angular 2 y la reescritura (2016)

El 14 de septiembre de 2016 se publicó Angular 2, una reescritura completa en TypeScript. No era una versión nueva, era otro framework: no había forma directa de actualizar, y la comunidad lo vivió como un trauma. Parte de quienes usaban AngularJS se marcharon a React, que estaba creciendo en esos años. La deuda de confianza que dejó aquel salto explica lo mucho que Angular cuida hoy las actualizaciones.

⚠️ Importante: “AngularJS” es la versión 1.x. “Angular”, sin más, es la 2 y posteriores. Son productos distintos. Si encuentras ng-app, $scope o ng-repeat, estás leyendo AngularJS y nada de eso te sirve aquí.

De esta etapa son también otras decisiones:

  • La versión 3 no existió. El paquete del router iba por la 3.x cuando el resto iba por la 2.x, y para alinearlos todos se saltó directamente a Angular 4 en marzo de 2017.
  • Se publicaron versiones mayores cada seis meses, con herramientas para actualizar de forma automática.
  • La organización del código se basaba en NgModule, una clase que agrupaba componentes, y la detección de cambios en Zone.js.

En Angular 9 (febrero de 2020) llegó Ivy, un motor de compilación y de renderizado nuevo, que es la base de lo que has visto en la sección 3. Fue un cambio enorme por dentro y casi invisible por fuera.

El renacimiento (2022 hasta hoy)

A finales de la década pasada, Angular tenía fama de pesado: mucha ceremonia para hacer poco. Desde 2022, el equipo fue retirando esa ceremonia, paso a paso y sin romper lo anterior.

Versión Fecha Hito
14 Junio de 2022 Componentes standalone en developer preview
15 Noviembre de 2022 Standalone estable: ya no hace falta NgModule
16 Mayo de 2023 Primeras señales, en developer preview
17 Noviembre de 2023 @if, @for, @switch, @defer; nuevo sitio angular.dev
18 Mayo de 2024 Zoneless experimental
19 Noviembre de 2024 Standalone por defecto; resource experimental
20 Mayo de 2025 API de señales estable; zoneless estable en la 20.2
21 Noviembre de 2025 Proyectos nuevos zoneless y con Vitest; Signal Forms experimental
22 Junio de 2026 OnPush por defecto, @Service, Signal Forms y resources estables, fetch por defecto, ciclo anual

El resultado es casi otro lenguaje de trabajo. Compara cómo se escribía un contador en 2019 y cómo se escribe hoy:

// Estilo de 2019: lo verás en el módulo 28 solo para saber leerlo
@Component({
  selector: 'app-contador',
  template: `
    <button (click)="sumar()">{{ cuenta }}</button>
    <p *ngIf="cuenta > 5">¡Muchos!</p>
  `,
})
export class ContadorComponent {
  @Input() cuenta = 0;
  @Output() cambio = new EventEmitter<number>();

  sumar() {
    this.cuenta++;
    this.cambio.emit(this.cuenta);
  }
}
// Además hacía falta declararlo en un NgModule
// Estilo actual
@Component({
  selector: 'app-contador',
  template: `
    <button (click)="sumar()">{{ cuenta() }}</button>
    @if (cuenta() > 5) {
      <p>¡Muchos!</p>
    }
  `,
})
export class Contador {
  readonly cuenta = model(0);

  sumar() {
    this.cuenta.update((n) => n + 1);
  }
}

Cómo reconocer la época de un tutorial

Si ves… Es de la época… Hoy se escribe
$scope, ng-repeat, ng-app AngularJS, otro producto No aplica
@NgModule, declarations Anterior a 2023 Componentes standalone con imports
*ngIf, *ngFor Anterior a la v17 @if, @for
@Input(), @Output() Anterior a las señales input(), output(), model()
constructor(private api: ApiService) Anterior a inject() inject(ApiService)
standalone: true v14 a v18 Se omite: es el valor por defecto
app.component.ts, AppComponent Anterior a la v20 app.ts, App
karma.conf.js Anterior a la v21 Vitest
zone.js en package.json Anterior a la v21 Nada: zoneless
@Injectable({ providedIn: 'root' }) Válido hasta hoy y en la LTS @Service() en 22

Todo el código antiguo que no figura como eliminado sigue funcionando. El módulo 28 te enseña a leerlo y a migrarlo.


5. Angular frente a React y Vue

Elegir herramienta no es elegir equipo de fútbol. Las tres son buenas opciones, y mucho de lo que aprendas aquí se transfiere: componentes, estado reactivo y flujo de datos. Las versiones comparadas son Angular 22.2, React 19.3 y Vue 3.5.

El mismo contador en los tres

Un botón que cuenta y muestra el doble. Los tres ejemplos están compilados y ejecutados.

Angular:

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-contador',
  template: `
    <button (click)="cuenta.set(cuenta() + 1)">
      {{ cuenta() }} (doble: {{ doble() }})
    </button>
  `,
})
export class Contador {
  protected readonly cuenta = signal(0);
  protected readonly doble = computed(() => this.cuenta() * 2);
}

React:

import { useState } from 'react';

export function Contador() {
  const [cuenta, setCuenta] = useState(0);
  const doble = cuenta * 2;

  return (
    <button onClick={() => setCuenta(cuenta + 1)}>
      {cuenta} (doble: {doble})
    </button>
  );
}

Vue:

<script setup lang="ts">
import { computed, ref } from 'vue';

const cuenta = ref(0);
const doble = computed(() => cuenta.value * 2);
</script>

<template>
  <button @click="cuenta++">{{ cuenta }} (doble: {{ doble }})</button>
</template>

Se parecen más de lo que dicen sus defensores. Las diferencias importantes están en lo que ocurre con ese código:

  • React vuelve a ejecutar la función entera cada vez que cambia el estado. doble se recalcula porque la función corre otra vez. La interfaz es el resultado de una función, y las reglas de los hooks existen para que ese modelo funcione.
  • Vue y Angular ejecutan el código de preparación una sola vez. ref en Vue y signal en Angular crean valores que saben quién los lee, de modo que solo se actualiza lo que depende de ellos. Por eso el modelo reactivo de Angular y Vue se parece hoy mucho.
  • Las plantillas cambian de sitio. React usa JSX, que es JavaScript. Vue y Angular usan plantillas HTML. Vue las guarda en un solo fichero .vue; Angular usa clases con plantilla interna o externa.

Comparación

Criterio Angular React Vue
Qué es Plataforma completa Librería de interfaces Framework progresivo
Quién lo mantiene Google Meta y su comunidad Evan You y su comunidad
Lenguaje TypeScript, obligado en la práctica JavaScript o TypeScript JavaScript o TypeScript
Vista Plantillas HTML con control de flujo JSX Plantillas HTML con directivas
Reactividad Señales Volver a ejecutar el componente ref y reactive
Enrutado Oficial, incluido Lo eliges tú (por ejemplo, React Router) Oficial, paquete aparte
Formularios Oficiales, tres modelos Lo eliges tú Lo eliges tú
HTTP Oficial, incluido Lo eliges tú Lo eliges tú
Inyección de dependencias Central, con jerarquía Contexto, más limitado provide/inject, sencillo
Herramienta de proyecto Angular CLI Vite o el framework que elijas create-vue, con Vite
Marco para SSR @angular/ssr, oficial Next.js y otros, de terceros Nuxt, de terceros
Curva de entrada Más empinada al principio Suave al principio, se complica con las decisiones La más suave
Actualizaciones Calendario público y migraciones automáticas Sin calendario fijo Sin calendario fijo
Punto fuerte Equipos grandes, vida larga Ecosistema enorme y flexibilidad Sencillez y buena documentación

📘 Concepto: El contraste de fondo es decidido frente a flexible. En Angular, muchas decisiones ya están tomadas: eso reduce libertad y también discusiones, y hace que las actualizaciones sean coordinadas. En React eliges cada pieza: tienes más libertad y más trabajo de integración. Ninguno de los dos enfoques es mejor en abstracto.

¿Cuándo tiene sentido Angular?

Encaja bien cuando:

  • La aplicación es grande, con muchas pantallas, formularios y permisos: paneles de gestión, banca, administración, intranets.
  • Trabajan muchos programadores, y conviene que el código sea uniforme.
  • El proyecto vivirá años, y valoras tener un calendario de soporte y migraciones automáticas.
  • El equipo viene de Java o C#: clases, decoradores, tipos e inyección de dependencias le resultarán familiares.

Encaja peor cuando:

  • Es una web de contenido casi estática, donde sobran un framework y su JavaScript.
  • Es un widget pequeño que se inserta en una página existente.
  • Dependes de un ecosistema concreto de librerías pensadas para React.

Si te interesa comparar en profundidad, en este mismo sitio hay un Curso de React con la misma aplicación EMPRESA.


6. Política de versiones

Angular publica versiones con un calendario público, y esta es una de sus ventajas frente a otras opciones. Para elegir versión con criterio, primero hay que entender qué promete cada número.

Versionado semántico

Las versiones tienen la forma mayor.menor.parche, como 22.2.4:

Parte Ejemplo Qué trae Qué tienes que hacer
Mayor 22.0.0 Funciones nuevas y posibles cambios incompatibles Actualizar con ng update y revisar
Menor 22.2.0 Funciones nuevas compatibles Nada: las nuevas funciones son opcionales
Parche 22.2.4 Correcciones de bajo riesgo Nada

Desde Angular 7, las versiones mayores del núcleo y de la CLI van alineadas. Fíjate en que los paquetes no tienen por qué coincidir en el último número: en este curso se usa @angular/core 22.2.4 con la CLI 22.2.5. Lo que debe coincidir es la versión mayor.

También existen versiones preliminares que no debes usar en un proyecto normal: next (en desarrollo, como 22.3.0-next.2) y rc (candidata a publicación). Este curso no las enseña.

El ciclo de publicación cambió en la v22

La documentación oficial describe este calendario:

Publicación Frecuencia
Mayor Cada 12 meses
Menor Entre 4 y 6 por cada mayor
Parche y preliminares Casi cada semana

⚠️ Importante: Si lees que “Angular saca una versión mayor cada seis meses”, esa información ya no está al día. Fue así hasta Angular 21, con entre 1 y 3 versiones menores por cada mayor. Desde la v22, el ciclo es anual. El cambio tiene sentido: menos saltos mayores que gestionar y más funciones que llegan en versiones menores, sin romper nada.

Esto se nota ya en la 22: la 22.1 llegó el 29 de julio de 2026 y la 22.2 el 23 de septiembre de 2026.

Activa y LTS

Cada versión mayor pasa por dos fases, y en total suele tener 24 meses de soporte:

Fase Duración Qué recibe
Activa 12 meses Versiones menores con novedades, y parches
LTS (long-term support) 12 meses Solo correcciones críticas y de seguridad

Las versiones 20 y 21 son de la etapa anterior, y por eso su fase activa fue más corta. Este es el calendario oficial, comprobado en noviembre de 2026:

Versión Estado Lanzamiento Fin de activa Fin de LTS
22 Activa 2026-06-03 2027-06 2028-06
21 LTS 2025-11-19 2026-06-03 2027-06
20 LTS 2025-05-28 2025-11-19 2026-11-28

Barras de calendario de las versiones 20, 21 y 22, con su fase activa y su fase LTS, y una línea en noviembre de 2026

Figura 10. Las tres versiones con soporte. La 20 está a punto de quedarse sin él.

Cuando el curso hable de “la LTS”, se refiere siempre a Angular 21.2, la LTS más reciente y la que seguirá viva más tiempo. La 20 también está en LTS, pero acaba el 28 de noviembre de 2026.

📘 Concepto: Una LTS no es una versión “mejor” ni “más estable”. Es una versión que ya no recibe novedades. Lo que publica son correcciones de seguridad y arreglos de regresiones graves, por ejemplo si un navegador nuevo rompe algo. Su valor es que no cambia. Su coste es que no mejora.

Estable, developer preview y experimental

Dentro de una misma versión no todas las APIs tienen la misma garantía:

Estado Qué significa ¿Lo uso en producción?
Estable Se aplica la política de obsolescencia: no desaparece sin aviso Sí
Developer preview Funciona y va camino de estabilizarse, pero puede cambiar, incluso en un parche Con cuidado, y fijando la versión
Experimental Funciona, pero podría no llegar a ser estable nunca Solo para probar
Obsoleta (deprecated) Sigue funcionando, pero está previsto que se elimine No en código nuevo

La regla sobre obsolescencia es clara: una API obsoleta permanece disponible al menos durante una versión mayor (alrededor de un año), solo se elimina en una versión mayor, y se anuncia en el registro de cambios y con la etiqueta @deprecated. Por eso el editor te la muestra tachada.

⚠️ Importante: Las garantías de versionado no cubren ni developer preview ni experimental. Una actualización de parche puede cambiar esas APIs. Es justo lo que le ocurre a Signal Forms en la LTS: allí es experimental, de modo que su forma no está garantizada.

Puedes comprobar el estado de una API tú mismo: pasa el cursor por encima en VS Code, o lee su comentario en el fichero .d.ts. Por ejemplo, en Angular 22.2:

API Estado en 22.2 Desde
signal, computed Estable Antes de la 21
resource, httpResource, rxResource Estable 22.0
Signal Forms (form, FormField…) Estable 22.0
Service, injectAsync, onIdle Estable 22.0
withAutoCleanupInjectors Estable 22.2
withRouterResources, nonBlocking Developer preview 22.2
withViewTransitions Developer preview 19.0
provideCheckNoChangesConfig Developer preview 20.0
debounced Experimental 22.0
WebMCP (provideExperimentalWebMcpTools…) Experimental 22
withFetch, withJsonpSupport Obsoleta 22

Cómo se actualiza

Angular tiene una orden pensada para eso:

ng update @angular/core@22 @angular/cli@22

No solo cambia números de versión: ejecuta migraciones que reescriben tu código cuando cambia una API o un comportamiento. La idea clave es que una migración conserva lo que tu programa hacía antes. Lo nuevo por defecto se aplica a los proyectos nuevos, y los existentes siguen comportándose igual hasta que tú decidas cambiarlos. Estas son algunas de las migraciones que incluye @angular/core al pasar a la 22:

Migración Qué hace en tu proyecto
change-detection-eager Añade ChangeDetectionStrategy.Eager a los componentes, para que no pasen a OnPush sin que lo sepas
http-xhr-backend Añade withXhr() a provideHttpClient cuando se usa HttpXhrBackend
strict-templates-default Escribe strictTemplates: false en tsconfig.json si no lo tenías indicado
incremental-hydration Añade withNoIncrementalHydration() donde no la usabas

Hay que dar los saltos de una versión mayor en una: de 20 a 21 y de 21 a 22, nunca de 20 a 22 directamente. El módulo 28 lo hace paso a paso.

⚠️ Importante: Por eso un proyecto actualizado desde la 21 está lleno de changeDetection: ChangeDetectionStrategy.Eager, mientras que uno creado con ng new en la 22 no lo tiene. No es un error de nadie: son dos puntos de partida distintos. Puedes ir quitando esa línea componente a componente, a medida que los pases a señales.


7. ¿Qué versión elegir?

Esta es la decisión práctica del módulo. El curso se escribe para Angular 22.2, la versión activa, y te recomendamos seguirlo con ella. Pero puede que tu empresa, tu centro o tu proyecto te obliguen a la LTS, y queremos que puedas seguirlo igualmente.

Árbol de decisión: proyecto nuevo o aprendizaje lleva a Angular 22.2; proyecto existente sin bloqueos a 22.2 con ng update; con bloqueos a 21.2 LTS

Figura 11. Cómo decidir la versión.

Criterios

Elige Angular 22.2 si:

  • Estás aprendiendo. Aprenderás la forma actual de trabajar, y el material antiguo te resultará fácil después.
  • Empiezas un proyecto nuevo. Tendrás soporte hasta junio de 2028, frente a junio de 2027.
  • Necesitas Signal Forms o resources con garantías: en la 22 son estables.

Elige Angular 21.2 LTS si:

  • Tu proyecto ya está en la 21 y alguna librería de la que dependes aún no admite la 22.
  • Tu entorno fija una versión de Node que Angular 22 no acepta. Por ejemplo, Node 20.
  • Tu organización exige no adoptar una versión mayor hasta que lleve cierto tiempo publicada.

Evita Angular 20 para empezar. Su soporte acaba el 28 de noviembre de 2026.

💡 Consejo: Quedarse en una LTS es una decisión con fecha de caducidad, no un destino. Cuando elijas la 21, apunta ya la fecha: en junio de 2027 tendrás que estar en la 22. El módulo 28 explica ese paso.

Compatibilidad de herramientas

  Angular 22.2 Angular 21.2 LTS
Node ^22.22.3 || ^24.15.0 || >=26.0.0 ^20.19.0 || ^22.12.0 || ^24.0.0
TypeScript >=6.0 <6.1 (ng new usa ~6.0.2) >=5.9 <6.0
RxJS ^6.5.3 || ^7.4.0 (~7.8.0) Igual
Pruebas por defecto Vitest 5 + jsdom 30 Vitest 4 + jsdom 28

⚠️ Importante: TypeScript 7 no está soportado en Angular 22. Si instalas typescript@latest a mano, la compilación fallará. Deja la versión que instala ng new.

Para crear un proyecto con la LTS, indica la versión al invocar la CLI:

npx @angular/cli@21 new empresa

Lo que no cambia

Antes de ver lo que pierdes, una buena noticia: lo esencial del curso es igual en las dos versiones. Todo esto ya existe en la LTS y funciona igual:

  • Componentes standalone, señales (signal, computed, effect, linkedSignal), input(), output(), model() e inject().
  • Control de flujo @if, @for, @switch, @let y @defer.
  • Zoneless: los proyectos nuevos de la 21 tampoco llevan Zone.js.
  • Vitest como herramienta de pruebas por defecto.
  • Ficheros app.ts, app.html y app.css sin sufijo.
  • El router, HttpClient, los formularios de plantilla y los reactivos.

Lo que pierdes en Angular 21

Todas estas diferencias están comprobadas compilando y ejecutando en 21.2 y 22.2, y con la comparación de las APIs públicas de ambas versiones.

Funciones que no existen en la LTS:

Función de 22 Desde En la LTS Qué hacer si usas la LTS Módulo
@Service() 22.0 Error TS2305 @Injectable({ providedIn: 'root' }) 08
injectAsync, onIdle, provideIdleServiceWith 22.0 Error TS2305 Importación dinámica con import() y un inyector propio; sin precarga en reposo integrada 08
withXhr 22.0 Error TS2305 No lo necesitas: XHR ya es el valor por defecto 12
HttpRequestOptions 22.0 No existe Escribe tu propio tipo con las opciones que uses 12
minDate, maxDate en Signal Forms 22.0 Error TS2305 Validador propio con validate 18
isFieldTree 22.1 No existe Comprueba el tipo por tu cuenta 18
linkedSignal con set 22.1 Error TS2353 Envuelve la escritura en un método propio 05
Comentarios // y /* */ entre atributos 22.0 Error NG5002 Sácalos a un comentario HTML, fuera de la etiqueta 04
Acceso a miembros private en la plantilla 22.2 Error TS2341 Usa protected 04
Bloques @boundary y @error 22.2 Error NG5002 Captura el error en un ErrorHandler o aísla el fallo en tu lógica 23
withAutoCleanupInjectors 22.2 Error TS2305 withExperimentalAutoCleanupInjectors, que en la 21 es experimental 11
containsTree 22.2 Error TS2305 Compara las URL con router.isActive 11
RedirectCommand lanzable 22.2 No disponible Devuelve el RedirectCommand desde el guard 11
withRouterResources, nonBlocking 22.2, dev preview Error TS2305 Carga los datos en un resolver o en el componente 11
debounced 22.0, experimental Error TS2305 Temporizador propio, o debounceTime de RxJS 05
WebMCP 22, experimental No existe Sin alternativa 27

Funciones que existen pero sin garantías:

Función En 22.2 En 21.2 LTS Consecuencia Módulo
resource, httpResource, rxResource Estables Experimentales Pueden cambiar sin aviso. Alternativa estable: HttpClient con toSignal 13
Signal Forms Estables Experimentales Pueden cambiar sin aviso. Alternativa estable: formularios reactivos 18
@angular/aria Estable Developer preview Puede cambiar sin aviso. Alternativa: patrones ARIA propios o Material 21

Valores por defecto distintos:

Comportamiento En 22.2 En 21.2 LTS Para igualarlo en la LTS Módulo
Detección de cambios sin indicar nada OnPush Default, también llamado Eager changeDetection: ChangeDetectionStrategy.OnPush 09
HttpClient Usa fetch Usa XHR provideHttpClient(withFetch()) 12
provideClientHydration() Hidratación incremental incluida Hay que pedirla provideClientHydration(withIncrementalHydration()) 24
strictTemplates Implícito ng new lo escribe en tsconfig.json Nada: el efecto es el mismo 01
ng g service Genera @Service() Genera @Injectable Nada: la CLI se adapta sola 08

Y en sentido contrario: lo que se va en la 22

Si vienes de código de la 21 o anterior, debes saber que algunas cosas ya no están:

Eliminado en 22 Sustituto
ComponentFactory, ComponentFactoryResolver createComponent o ViewContainerRef.createComponent
ModuleWithComponentFactories Sin sustituto: ya no hace falta
createNgModuleRef createNgModule
provideRoutes provideRouter
WithField, WithOptionalField, WithoutField (Signal Forms) WithFieldTree, WithOptionalFieldTree, WithoutFieldTree

Y otras siguen ahí, pero están obsoletas: withFetch (porque ya es el valor por defecto), JSONP (withJsonpSupport, JsonpInterceptor), withExperimentalAutoCleanupInjectors, y disabled, hidden y readonly como funciones de esquema de Signal Forms. Lo verás en cada módulo correspondiente, y en conjunto en el módulo 28.


8. Requisitos para seguir el curso

Conocimientos

Necesitas Nivel Dónde repasarlo
HTML y CSS Etiquetas, formularios, selectores, flexbox Curso de HTML y CSS
JavaScript Funciones flecha, map/filter, promesas, módulos, clases, fetch Curso de JavaScript
TypeScript No hace falta: el módulo 02 enseña lo necesario  
Terminal Moverte por carpetas y ejecutar órdenes  
HTTP y JSON Métodos, códigos de estado, formato JSON Se repasa en el módulo 12

No necesitas saber nada de React, ni de RxJS. Si vienes de Java o C#, las clases, los decoradores y la inyección de dependencias te resultarán familiares.

Herramientas

Herramienta Para qué Versión
Node.js Ejecutar la CLI y el servidor de desarrollo Una compatible con tu versión de Angular. Node 24 sirve para las dos versiones del curso
npm Instalar paquetes Viene con Node
Visual Studio Code Editor Última, con la extensión Angular Language Service
Chrome, Edge o Firefox Probar Última, con Angular DevTools
Git Control de versiones Cualquiera reciente

Node solo hace falta para desarrollar y compilar. Una aplicación Angular compilada es HTML, CSS y JavaScript estáticos, que se pueden publicar en cualquier servidor web sin Node, salvo que uses renderizado en servidor.

La instalación paso a paso está en el módulo 01. De momento basta con que compruebes tu versión de Node:

node -v
npm -v

El ejercicio 1 te da un pequeño programa que te dice si tu Node admite cada versión de Angular.

Las versiones de este curso

Todo el código del curso se ha compilado y ejecutado con estas versiones, que son las que muestra ng version en el proyecto de pruebas:

@angular/cli              22.2.5
@angular/build            22.2.5
@angular/core             22.2.4
@angular/common           22.2.4
@angular/compiler         22.2.4
@angular/forms            22.2.4
@angular/router           22.2.4
@angular/material         22.2.5
@angular/aria             22.2.5
rxjs                      7.8.2
typescript                6.0.3
vitest                    5.0.5

Si dentro de unos meses tienes versiones de parche o menores más altas, todo debería funcionar: es lo que promete el versionado semántico. Si un ejemplo que usa una función experimental falla, revisa esa API en la documentación de angular.dev, porque esas son las únicas que pueden haber cambiado.


9. La API de ejemplo EMPRESA

A lo largo del curso construirás una aplicación de gestión de una empresa ficticia. Usa los mismos datos que los cursos de Bases de Datos y de Java Avanzado: empleados, departamentos, productos, clientes, pedidos y proyectos. Para que no tengas que montar un servidor, dispones de una API REST ya hecha, sin dependencias.

Puesta en marcha

  1. Descarga empresa-api.zip y descomprímelo en una carpeta.
  2. Abre una terminal en esa carpeta y ejecuta el servidor:
node server.mjs

La API queda en http://localhost:3000/api. Pruébala en el navegador abriendo http://localhost:3000/api/departamentos/30:

{"deptId":30,"nombre":"INFORMÁTICA","localidad":"Valencia","presupuesto":420000}

Los datos viven en memoria. Si reinicias el servidor, vuelven a su estado inicial, de modo que puedes probar a borrar sin miedo.

Qué contiene

Recurso Ruta Identificadores
Departamentos /api/departamentos 10, 20… hasta 70
Empleados /api/empleados Desde 100
Productos /api/productos Desde 1
Clientes /api/clientes Desde 1001
Pedidos /api/pedidos Desde 5001
Sesión /api/auth/login Devuelve un token

Los listados devuelven una página con esta forma, y admiten búsqueda (?q=garcia), orden (?orden=-salario) y paginación (?pagina=2&tamano=5):

{ "contenido": [], "pagina": 1, "tamano": 20, "total": 7, "paginas": 1 }

Las lecturas son públicas. Las escrituras exigen iniciar sesión, por ejemplo como ana.garcia@empresa.es con la contraseña secreto. Lo usarás en el módulo 12 y en el proyecto final. El fichero README.md del ZIP documenta todas las rutas.

💡 Consejo: La variable RETARDO añade una espera a cada respuesta, y es muy útil para ver los estados de carga. Por ejemplo, en Git Bash: RETARDO=1500 node server.mjs.


10. Mapa del curso

El curso tiene 30 módulos, del 00 al 29, organizados en seis bloques. Se pueden leer en orden o por partes: cada módulo funciona por sí mismo.

Seis bloques del curso enlazados en orden: puesta en marcha, núcleo, aplicaciones reales, formularios y estado, interfaz y producción, y cierre

Figura 12. Los seis bloques del curso.

Módulo Título Qué aprenderás
00 Introducción a Angular Este módulo
01 Entorno y Angular CLI Instalar, crear proyectos, ng y sus órdenes
02 TypeScript para Angular Tipos, clases, decoradores, genéricos
03 Componentes Anatomía, estilos, ciclo de vida
04 Plantillas Enlaces, eventos, @if, @for, @switch, @let
05 Señales signal, computed, effect, linkedSignal
06 Comunicación entre componentes input(), output(), model(), proyección
07 Directivas y pipes Directivas propias, composición, Intl
08 Servicios e inyección de dependencias inject(), @Service, jerarquía de inyectores
09 Detección de cambios Zoneless, OnPush, depuración
10 Router Rutas, parámetros, carga perezosa
11 Router avanzado Guards, resolvers, precarga, eventos
12 HttpClient CRUD, interceptores, errores, progreso
13 Resources resource, httpResource, rxResource
14 RxJS fundamental Observables y operadores
15 RxJS y señales toSignal, toObservable, buscadores
16 Formularios por plantilla ngModel y validación
17 Formularios reactivos FormGroup, validadores, FormArray
18 Signal Forms form, schema, FormField
19 Gestión del estado Servicios con señales, NgRx
20 Angular Material y CDK Tema M3, tablas, diálogos, CDK
21 Angular Aria y accesibilidad Patrones accesibles, foco, teclado
22 Estilos: Tailwind en Angular CSS en componentes, Tailwind 4, animaciones
23 Rendimiento y @defer Vistas diferibles, imágenes, bundle
24 SSR e hidratación Render en servidor, prerender, hidratación
25 Internacionalización y PWA Traducción, service worker
26 Testing Vitest, TestBed, Playwright
27 Despliegue y herramientas con IA Docker, CI, seguridad, MCP
28 Migraciones y Angular clásico Leer código antiguo, ng update
29 Proyecto final La aplicación EMPRESA completa

Itinerarios

No todo el mundo busca lo mismo:

Si quieres… Sigue este camino
Aprender Angular desde cero En orden, del 00 al 19, y luego el 29
Hacer aplicaciones CRUD cuanto antes 01 a 06, 08, 10, 12, 13, 18 y 29
Ponerte al día si ya sabías Angular clásico 05, 06, 08, 09, 13, 18 y 28
Trabajar en un proyecto antiguo 01, 03 a 05, 14 a 17 y 28
Preparar la puesta en producción 23 a 27

💡 Consejo: Escribe el código de los ejemplos, aunque sea más lento que copiarlo. Y haz los ejercicios antes de abrir la solución: equivocarte y ver el error del compilador enseña mucho más que leer una respuesta correcta.

Cómo leer las marcas de versión

Como el curso se ofrece con Angular 22.2, pero se puede seguir con la LTS, todo lo que cambia entre ambas está marcado. Las marcas aparecen al final de un encabezado:

Marca Significado Ejemplo
🆕 v22.0 La función no existe en Angular 21 @Service
🔄 v22.0 La función existe en Angular 21, pero cambia: de estado, de comportamiento por defecto o de sintaxis httpResource, OnPush

El número indica en qué versión llegó el cambio. Justo debajo del encabezado hay siempre una caja que te dice qué hacer. Ya has visto una en la sección 2:

🆕 Desde Angular 22.0. No existe en la LTS (Angular 21). Si usas la LTS: importa Injectable y escribe @Injectable({ providedIn: 'root' }) en lugar de @Service(); el resto del ejemplo compila igual.

Las reglas para interpretarlas son simples:

  • Si un apartado no tiene marca, funciona igual en la LTS. No tienes que comprobar nada.
  • Si usas Angular 22, puedes ignorar la caja. Solo te interesa como información.
  • Si usas la LTS, aplica lo que dice “Si usas la LTS”. Si no hay alternativa, la caja te dice qué se pierde.
  • Si la marca está en el encabezado de un módulo, afecta a todo el módulo, y aparece también una caja bajo los objetivos.

Las funciones que siguen sin ser estables en la 22 se indican en el texto con su estado real: developer preview o experimental.

Las cajas del resto del curso significan esto:

📘 Concepto: Una idea que debes retener.

💻 Ejemplo: Un caso práctico.

✏️ Ejercicio: Algo que hacer sobre la marcha.

⚠️ Importante: Un error frecuente o una trampa.

💡 Consejo: Una recomendación práctica.


11. Errores frecuentes al empezar

Síntoma Causa Solución
Sigues un tutorial y aparece @NgModule o app.module.ts, que tu proyecto no tiene Tutorial anterior a standalone Busca material actual, o mira la tabla de la sección 4
ng-repeat o $scope no hacen nada Es AngularJS, otro producto Usa @for y señales
TS2305: Module '"@angular/core"' has no exported member 'Service' Estás en la LTS y el código es para 22 Aplica la caja “Si usas la LTS”
La CLI dice que tu versión de Node no es compatible Node fuera de los rangos admitidos Actualiza Node; la tabla está en la sección 7
Falla la compilación tras instalar TypeScript 7 Angular 22 solo admite TypeScript 6.0 Vuelve a ~6.0.2
Un dato cambia, pero la pantalla no No está en una señal, y el componente es OnPush Guárdalo en una señal (ejercicio 9)
No se ha podido contactar con la API La API no está arrancada, o usa otro puerto Ejecuta node server.mjs en otra terminal
Saltas de la 20 a la 22 y aparecen errores ng update debe ir de versión en versión Actualiza primero a la 21
Una API experimental deja de funcionar al actualizar un parche Sin garantías de versionado Fija la versión exacta, o evita esas APIs en producción

12. Ejercicios

Ejercicio 1: ¿Tu Node vale?

Escribe un programa comprobar-node.mjs que diga si tu versión de Node es compatible con Angular 22.2 y con Angular 21.2, usando la tabla de la sección 7. Lee la versión con process.versions.node. Ejecútalo con node comprobar-node.mjs.

Ver solución
// Dice qué versiones de Angular admiten el Node con el que se ejecuta
const requisitos = {
  'Angular 22.2': [[22, 22, 3], [24, 15, 0], [26, 0, 0]],
  'Angular 21.2': [[20, 19, 0], [22, 12, 0], [24, 0, 0]],
};

function admitida(actual, rangos) {
  const [mayor, menor, parche] = actual;
  return rangos.some(([m, n, p]) => {
    // El último rango de Angular 22 es ">=26.0.0": vale cualquier mayor superior
    const esAbierto = m === 26;
    if (esAbierto && mayor > m) return true;
    if (mayor !== m) return false;
    if (menor !== n) return menor > n;
    return parche >= p;
  });
}

const actual = process.versions.node.split('.').map(Number);
console.log(`Node ${process.versions.node}`);
for (const [angular, rangos] of Object.entries(requisitos)) {
  console.log(`  ${angular}: ${admitida(actual, rangos) ? 'compatible' : 'NO compatible'}`);
}

Con Node 24.19:

Node 24.19.0
  Angular 22.2: compatible
  Angular 21.2: compatible

Cada rango ^24.15.0 significa “misma versión mayor y al menos esa versión”. Por eso se compara primero la mayor, luego la menor y luego el parche. Observa que Node 24.10 valdría para Angular 21, pero no para Angular 22, y que las versiones impares como Node 23 no valen para ninguna de las dos.

Ejercicio 2: Calendario de soporte

Escribe un programa soporte.mjs que reciba una fecha y diga, para las versiones 20, 21 y 22, si en ese día están activas, en LTS o sin soporte. Para los fines de soporte que solo se dan por mes, usa el último día del mes. Pruébalo con el 15 de diciembre de 2026 y con el 15 de julio de 2027.

Ver solución
// Uso: node soporte.mjs [fecha]    Ejemplo: node soporte.mjs 2026-12-15
const calendario = [
  { version: 22, lanzamiento: '2026-06-03', finActivo: '2027-06-30', finLts: '2028-06-30' },
  { version: 21, lanzamiento: '2025-11-19', finActivo: '2026-06-03', finLts: '2027-06-30' },
  { version: 20, lanzamiento: '2025-05-28', finActivo: '2025-11-19', finLts: '2026-11-28' },
];

function estado(version, fecha) {
  if (fecha < version.lanzamiento) return 'aún no publicada';
  if (fecha <= version.finActivo) return 'activa';
  if (fecha <= version.finLts) return 'LTS';
  return 'sin soporte';
}

const fecha = process.argv[2] ?? new Date().toISOString().slice(0, 10);
console.log(`Estado a ${fecha}:`);
for (const version of calendario) {
  console.log(`  Angular ${version.version}: ${estado(version, fecha)}`);
}
Estado a 2026-12-15:
  Angular 22: activa
  Angular 21: LTS
  Angular 20: sin soporte
Estado a 2027-07-15:
  Angular 22: LTS
  Angular 21: sin soporte
  Angular 20: sin soporte

Las fechas se comparan como texto, y funciona porque el formato ISO AAAA-MM-DD ordena igual alfabética y cronológicamente. La salida confirma lo que dice la sección 7: en julio de 2027 quien siga en la 21 estará sin soporte.

Ejercicio 3: Filtrar también por nombre

Modifica el componente App de la sección 2 para que el filtro busque en la localidad o en el nombre del departamento. Al escribir ven debe aparecer solo VENTAS.

Ver solución

Solo cambia visibles:

protected readonly visibles = computed(() => {
  const texto = this.filtro().trim().toLowerCase();
  return this.departamentos
    .todos()
    .filter(
      (d) =>
        d.localidad.toLowerCase().includes(texto) ||
        d.nombre.toLowerCase().includes(texto),
    );
});

La plantilla no se toca. Es una ventaja de computed: la vista depende del resultado, no de cómo se obtiene. Observa además que el nombre VENTAS va en mayúsculas y aun así coincide, porque comparamos los dos textos en minúsculas.

Prueba a escribir valencia y ven para comprobarlo. ven no coincide con ninguna localidad, solo con VENTAS.

Ejercicio 4: Departamentos sin presupuesto

Añade al componente una señal derivada sinPresupuesto que cuente los departamentos cuyo presupuesto es null, y muéstrala en la plantilla con el texto 1 sin presupuesto.

Ver solución
protected readonly sinPresupuesto = computed(
  () => this.departamentos.todos().filter((d) => d.presupuesto === null).length,
);
<p>{{ sinPresupuesto() }} sin presupuesto</p>

Se usa === null, y no !d.presupuesto, porque un presupuesto de 0 sería un dato válido, distinto de “sin presupuesto”. Con los datos de EMPRESA el resultado es 1: INVESTIGACIÓN.

Como sinPresupuesto depende solo de la lista, no se recalcula al escribir en el filtro.

Ejercicio 5: Adaptar el ejemplo a la LTS

Quieres compilar el ejemplo de la sección 2 en Angular 21.2. Di exactamente qué líneas cambias para que compile, y qué línea añadirías para que además se comporte igual que en Angular 22.

Ver solución

Para que compile, basta con cambiar el servicio:

// Angular 22
import { Service, computed, signal } from '@angular/core';

@Service()
export class Departamentos {
// Angular 21.2
import { Injectable, computed, signal } from '@angular/core';

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

Sin este cambio, el compilador se detiene con:

TS2305: Module '"@angular/core"' has no exported member 'Service'.

Para que el comportamiento sea el mismo, añade OnPush a cada componente:

import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  imports: [CurrencyPipe],
  templateUrl: './app.html',
  styleUrl: './app.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})

En este ejemplo no se nota la diferencia, porque todo lo que se muestra está en señales. Aun así conviene añadirlo: tu código se comportará igual si después actualizas a la 22, y los fallos por datos que no están en señales se verán desde el principio. El ejemplo de httpResource no necesita cambios para compilar en la 21, aunque allí esa API es experimental.

Ejercicio 6: Interpretar rangos de versiones

En package.json aparecen versiones como ^22.2.4 o ~22.2.4. El símbolo ^ admite versiones compatibles dentro de la misma mayor, y ~ admite solo parches. Indica para cada rango si acepta 22.2.4, 22.2.9, 22.3.0, 23.0.0 y 22.1.0.

Ver solución

Comprobado con la librería semver, la misma que usa npm:

Versión ^22.2.4 ~22.2.4 22.2.4
22.2.4 Sí Sí Sí
22.2.9 Sí Sí No
22.3.0 Sí No No
23.0.0 No No No
22.1.0 No No No

Por eso los paquetes de Angular suelen ir con ^: admiten mejoras compatibles. En cambio, ng new escribe TypeScript con ~6.0.2: una versión menor de TypeScript puede romper la compilación, y Angular declara exactamente qué versiones admite. Si usas una API experimental y no quieres sorpresas, fija la versión exacta, sin símbolo.

Ejercicio 7: Elige la versión

Decide qué versión usarías en cada caso y justifícalo.

  1. Una tienda de barrio quiere una aplicación nueva, y tú la mantendrás dos años.
  2. Un banco tiene una aplicación en Angular 21 con una librería de componentes de pago que aún no admite la 22.
  3. Un servidor de integración continua usa Node 20 y no se puede cambiar este año.
  4. Un proyecto sigue en Angular 20 en noviembre de 2026.
  5. Quieres aprender Angular para buscar trabajo.
Ver solución
  1. Angular 22.2. Proyecto nuevo, sin ataduras: tendrá soporte hasta junio de 2028, y por tanto cubre los dos años si actualizas después a la siguiente versión mayor.
  2. Angular 21.2 LTS, de momento. Está soportada hasta junio de 2027. Hay que reclamar al proveedor una versión compatible y fijar una fecha para el salto.
  3. Angular 21.2 LTS. Angular 22 no admite Node 20; la 21 sí admite ^20.19.0. Comprueba además que tienes al menos Node 20.19. Node 20 condiciona toda la pila.
  4. Actualizar ya a la 21, y después a la 22. Su soporte acaba el 28 de noviembre de 2026. Quedarse significa no recibir correcciones de seguridad.
  5. Angular 22.2. Muchas empresas tendrán código antiguo, y por eso conviene aprender además a leerlo, con el módulo 28. Pero lo que aprendes con la 22 se transfiere casi por completo a la 21, y el camino inverso es más largo.

Ejercicio 8: Clasifica cada API

Sin mirar la sección 6, clasifica estas APIs de Angular 22.2 como estables, developer preview, experimentales u obsoletas. Después indica cuáles usarías en producción: httpResource, debounced, withFetch, withRouterResources, Service, withViewTransitions.

Ver solución
API Estado en 22.2 ¿En producción?
httpResource Estable desde la 22.0 Sí
debounced Experimental No: puede cambiar o desaparecer. Usa un temporizador propio o debounceTime
withFetch Obsoleta No hace falta: fetch ya es el valor por defecto. Quítalo
withRouterResources Developer preview Con cuidado y fijando la versión
Service Estable desde la 22.0 Sí
withViewTransitions Developer preview Con cuidado. Lleva en este estado desde la 19, lo que recuerda que preview no implica una fecha

Para comprobarlo en tu proyecto, abre el fichero .d.ts de la API con F12 en VS Code. Encontrarás etiquetas como @experimental 22.0 o @developerPreview 22.2 en su comentario. La versión estable suele llevar @publicApi.

Ejercicio 9: El hijo que no se actualiza

Este código muestra un número que está en un objeto normal, no en una señal. Se modifica compartido.visitas y a continuación se pulsa el botón del padre. ¿Qué muestra el hijo en Angular 22? ¿Y en Angular 21? Arregla el hijo para que funcione igual en ambas.

const compartido = { visitas: 0 };

@Component({ selector: 'app-hijo', template: `<p id="hijo">{{ datos.visitas }}</p>` })
class Hijo {
  datos = compartido;
}

@Component({
  imports: [Hijo],
  template: `<button (click)="n.set(n() + 1)">{{ n() }}</button><app-hijo />`,
})
class Padre {
  n = signal(0);
}
Ver solución

Lo hemos ejecutado en las dos versiones, con compartido.visitas = 5 y un clic en el botón:

Versión Lo que muestra el hijo Por qué
Angular 22.2 0 Hijo es OnPush por defecto. Nada le ha dicho que deba revisarse: sus input no cambian, ninguna señal suya cambia y el evento no ha ocurrido en él
Angular 21.2 5 Hijo es Default. Se revisa cada vez que se revisa su padre

Este es el motivo del cambio de la 22: el código “funciona” en la 21, pero por casualidad, gracias a un clic ajeno. Lo que muestra depende de que otro componente provoque una revisión.

La solución en las dos versiones es usar una señal:

export const visitas = signal(0);

@Component({ selector: 'app-hijo-senal', template: `<p id="hijo">{{ visitas() }}</p>` })
export class HijoSenal {
  protected readonly visitas = visitas;
}

Ahora visitas.set(5) actualiza el hijo sin necesidad de ningún clic, en Angular 21 y en 22. Las señales avisan por sí mismas.

Ejercicio 10: Consulta la API desde Node

Con la API en marcha, escribe listar-api.mjs que use fetch para mostrar los 3 departamentos con más presupuesto y el total de departamentos. Utiliza los parámetros de la sección 9.

Ver solución
const url = process.argv[2] ?? 'http://localhost:3000/api';

const respuesta = await fetch(`${url}/departamentos?orden=-presupuesto&tamano=3`);
const pagina = await respuesta.json();

console.log(`Total: ${pagina.total}. Los 3 primeros por presupuesto:`);
for (const d of pagina.contenido) {
  console.log(`  ${d.deptId} ${d.nombre} (${d.localidad}): ${d.presupuesto}`);
}
Total: 7. Los 3 primeros por presupuesto:
  10 DIRECCIÓN (Madrid): 500000
  30 INFORMÁTICA (Valencia): 420000
  20 VENTAS (Madrid): 350000

El - delante de presupuesto pide orden descendente. total sigue siendo 7 aunque tamano sea 3, porque cuenta todos los elementos, no solo los de esa página. Es lo que permite montar una paginación en la interfaz. El await en la raíz del fichero funciona porque es un módulo (.mjs).

Ejercicio 11 (propuesto): Presupuesto de lo visible

Añade al ejemplo de la sección 2 una señal derivada que sume el presupuesto solo de los departamentos visibles, y muéstrala junto al total. Comprueba que se actualiza al filtrar y que el total de la empresa no cambia.

Ejercicio 12 (propuesto): Tu tabla de decisión

Piensa en un proyecto real de tu entorno: un trabajo de clase, las prácticas o la web de un conocido. Rellena una tabla con tamaño del equipo, vida esperada, necesidades de formularios y SEO. Decide entre Angular, React o Vue, y si eliges Angular, qué versión. Argumenta tu decisión en cinco líneas.


Resumen

Concepto API Idea clave
Angular @angular/*, ng Plataforma completa con TypeScript: los paquetes oficiales avanzan juntos
SPA index.html, <app-root> Una sola página; el JavaScript cambia las vistas
Componente @Component Clase con plantilla y estilos
Señal signal, computed Valor que sabe quién lo lee y avisa al cambiar
Servicio @Service 🆕 v22.0, inject() Lógica compartida; se pide, no se crea
Plantilla {{ }}, (evento), @if, @for HTML ampliado que se compila a JavaScript
Arranque bootstrapApplication(App, appConfig) De main.ts al componente raíz
Compilación AOT ng build Separa la parte fija de la dinámica, y comprueba tipos
Zoneless Sin zone.js La detección la activan avisos concretos, no cualquier tarea
OnPush Por defecto 🔄 v22.0 Un dato mostrado debe estar en una señal
Datos remotos httpResource 🔄 v22.0 Un objeto de señales: cargando, error y valor
AngularJS ng-app, $scope Es otro producto, ya retirado
Versionado mayor.menor.parche Solo las mayores pueden romper; lo experimental puede cambiar siempre
Ciclo Una mayor cada 12 meses Anual desde la v22; 12 meses activa y 12 de LTS
Versión del curso Angular 22.2 Activa, soporte hasta junio de 2028
LTS de referencia Angular 21.2 Solo correcciones hasta junio de 2027
Marcas 🆕 🔄 Sin marca, funciona igual en la LTS
Actualizar ng update Una versión mayor cada vez, con migraciones automáticas
API de ejemplo node server.mjs http://localhost:3000/api, datos en memoria