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.htmlhasta 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.
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.

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 empresay sustituye los ficheros desrc/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
Injectabley 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 delistay lo recalcula solo cuandolistacambia.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
changeDetectionusa OnPush; en la LTS (Angular 21) el valor por defecto esDefault, que Angular 21.2 también llamaEager. Si usas la LTS: añadechangeDetection: ChangeDetectionStrategy.OnPusha 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);
}
}
@Componentconvierte la clase en un componente.selectores la etiqueta HTML que lo representa,importslista lo que usa su plantilla, ytemplateUrlystyleUrlapuntan a sus ficheros.inject(Departamentos)pide el servicio. El componente no hacenew: lo pide y Angular se lo entrega.filtroes el texto que ha escrito el usuario, en una señal.visiblesdepende de dos señales,filtroy 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.protecteddeja 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:
- El evento
inputllama aalEscribir, que hacefiltro.set('madrid'). - La señal
filtroavisa de que ha cambiado.visiblesqueda marcada como pendiente de recalcular. - Angular revisa la vista que lee
visibles. Al hacerlo,computedejecuta el filtro y devuelve dos departamentos. @forcompara la lista nueva con la anterior usandotrack d.deptId: conserva los nodos deDIRECCIÓNyVENTASy elimina los otros cinco.- La interpolación actualiza “2 de 7 departamentos”. El total no cambia, porque
presupuestoTotalno depende defiltro.

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:

Figura 4. La rama @else, con el texto real que produce la plantilla.
💡 Consejo: Observa que en el presupuesto total
INVESTIGACIÓNcuenta como 0, porque su presupuesto esnully el operador??lo sustituye. Es el dato real de la API: un buen ejemplo de por qué conviene que el tiponumber | nullte obligue a pensar en ese caso.
Pedir los datos a la API con httpResource 🔄 v22.0
🔄 Cambia en Angular 22.0.
httpResourcepasa 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, usaHttpClientcontoSignal, 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.

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

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:
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
windowsobrevive 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 & 1crea el DOM una sola vez. El bloquerf & 2actualiza únicamente lo que puede cambiar: el títuloh1no aparece ahí porque es texto fijo. - Se comprueban los tipos. El compilador sabe que
ctx.visibles()devuelveDepartamento[]. Si escribesd.nombren 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.
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.
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,$scopeong-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.
doblese 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.
refen Vue ysignalen 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 | 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 |
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 conng newen 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.
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@latesta mano, la compilación fallará. Deja la versión que instalang 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()einject(). - Control de flujo
@if,@for,@switch,@lety@defer. - Zoneless: los proyectos nuevos de la 21 tampoco llevan Zone.js.
- Vitest como herramienta de pruebas por defecto.
- Ficheros
app.ts,app.htmlyapp.csssin 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
- Descarga empresa-api.zip y descomprímelo en una carpeta.
- 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
RETARDOañ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.
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
Injectabley 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.
- Una tienda de barrio quiere una aplicación nueva, y tú la mantendrás dos años.
- Un banco tiene una aplicación en Angular 21 con una librería de componentes de pago que aún no admite la 22.
- Un servidor de integración continua usa Node 20 y no se puede cambiar este año.
- Un proyecto sigue en Angular 20 en noviembre de 2026.
- Quieres aprender Angular para buscar trabajo.
Ver solución
- 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.
- 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.
- 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. - 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.
- 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 |
| Navegación | |
|---|---|
| Siguiente módulo: Entorno y Angular CLI → |