Angular: Desarrollo Frontend Empresarial
Guía completa de la arquitectura empresarial de Angular: módulos, standalone components, inyección de dependencias, programación reactiva con RxJS, signals, detección de cambios zoneless, SSR, i18n, formularios, testing y estrategias de migración. Basada en 4 aplicaciones Angular abarcando versiones 5 a 20/21.
Índice de Contenidos
- 1. Módulos, Standalone Components, Servicios e Inyección de Dependencias
- 2. RxJS y Programación Reactiva
- 3. Lazy Loading, Route Guards y Resolvers
- 4. Formularios: Reactivos vs Template-Driven
- 5. Testing de Aplicaciones Angular
- 6. Angular CLI y Schematics
- 7. Signals y Primitivas Reactivas
- 8. Renderizado del Lado del Servidor con Angular Universal
- 9. Internacionalización (i18n)
- 10. Estrategias de Migración
1. Módulos, Standalone Components, Servicios e Inyección de Dependencias
NgModules y el Sistema de Módulos
El sistema de módulos de Angular (@NgModule) organiza la aplicación en bloques cohesivos de funcionalidad. Cada módulo declara sus componentes, directivas y pipes; importa otros módulos de los que depende; y exporta ítems que otros módulos pueden usar. El AppModule arranca la aplicación, mientras que los feature modules encapsulan código específico del dominio. Con Angular 14+, los standalone components pueden omitir NgModules por completo, simplificando el modelo mental.@NgModule) organizes the application into cohesive blocks of functionality. Each module declares its components, directives, and pipes; imports other modules it depends on; and exports items that other modules can use. The AppModule bootstraps the application, while feature modules encapsulate domain-specific code. With Angular 14+, standalone components can bypass NgModules entirely, simplifying the mental model.
// Feature module: self-contained domain boundary
@NgModule({
declarations: [
MemberListComponent,
MemberDetailComponent,
MemberFormComponent,
MemberStatusPipe,
HighlightDirective,
],
imports: [
CommonModule,
ReactiveFormsModule,
MembersRoutingModule,
SharedModule, // shared UI components
],
providers: [
MemberService,
MemberResolver,
{ provide: MEMBER_CONFIG, useValue: { pageSize: 20, cacheTime: 300 } },
],
})
export class MembersModule {}
Standalone Components (Angular 14+)
Los standalone components declaran sus propias dependencias directamente, eliminando la necesidad de un NgModule envolvente. Cada standalone component, directiva o pipe especifica sus imports en línea. Esto simplifica el grafo de dependencias, reduce el boilerplate y hace el tree-shaking más efectivo. En Angular 17+, standalone es el valor por defecto para componentes recién generados.
// Standalone component: self-contained, no NgModule required
@Component({
selector: 'app-member-card',
standalone: true,
imports: [CommonModule, RouterLink, DatePipe, MemberStatusPipe],
template: `
<div class="card">
<h3>{{ member.name }}</h3>
<p>Status: {{ member.status | memberStatus }}</p>
<p>Joined: {{ member.joinDate | date:'mediumDate' }}</p>
<a [routerLink]="['/members', member.id]">View Details</a>
</div>
`,
})
export class MemberCardComponent {
@Input({ required: true }) member!: Member;
}
// Bootstrapping a standalone application (no AppModule)
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([authInterceptor, retryInterceptor])),
provideAnimations(),
],
});
Inyección de Dependencias: El Mecanismo Central
El sistema DI de Angular es jerárquico: los injectors forman un árbol que refleja el árbol de componentes. Un servicio provisto en root es un singleton en toda la aplicación. Un servicio provisto en un módulo lazy-loaded obtiene su propia instancia. Un servicio provisto en un componente crea una nueva instancia para ese componente y sus hijos. Entender esta jerarquía es esencial para controlar el ciclo de vida de los servicios y evitar bugs de estado compartido.root is a singleton across the entire application. A service provided in a lazy-loaded module gets its own instance. A service provided in a component creates a new instance for that component and its children. Understanding this hierarchy is essential for controlling service lifetimes and avoiding shared state bugs.
// Service with configurable scope
@Injectable({ providedIn: 'root' }) // Application singleton
export class AuthService {
private user$ = new BehaviorSubject<User | null>(null);
readonly currentUser$ = this.user$.asObservable();
constructor(private http: HttpClient, private router: Router) {}
login(credentials: LoginDto): Observable<User> {
return this.http.post<AuthResponse>('/api/auth/login', credentials).pipe(
tap(res => {
localStorage.setItem('token', res.token);
this.user$.next(res.user);
}),
map(res => res.user),
catchError(err => {
this.user$.next(null);
return throwError(() => new AuthError(err.error.message));
}),
);
}
}
// Component-scoped service: new instance per component
@Component({
selector: 'app-member-form',
providers: [FormValidationService], // New instance per form
templateUrl: './member-form.component.html',
})
export class MemberFormComponent {
constructor(private validation: FormValidationService) {}
}
Injection Tokens y Factories
Para objetos de configuración, clases abstractas y dependencias que no son clases, usa InjectionToken. Los factory providers habilitan la creación dinámica de servicios basada en condiciones de runtime. Los multi-providers permiten múltiples valores para el mismo token, útiles para sistemas de plugins y cadenas de interceptores HTTP.InjectionToken. Factory providers enable dynamic service creation based on runtime conditions. Multi-providers allow multiple values for the same token, useful for plugin systems and HTTP interceptor chains.
// InjectionToken with factory provider
const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', {
providedIn: 'root',
factory: () => environment.production
? 'https://api.example.com'
: 'http://localhost:3000',
});
// Multi-provider for HTTP interceptors
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: RetryInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
2. RxJS y Programación Reactiva
RxJS es la columna vertebral de Angular para manejar flujos de datos asíncronos. Cada request HTTP, cambio de valor de formulario, parámetro de ruta y mensaje WebSocket es un Observable. El poder reside en los operadores que componen, transforman, filtran y combinan streams. Dominar RxJS es la habilidad individual más impactante para el desarrollo Angular.
Patrones de Operadores Esenciales
// Search with debounce, distinct, switchMap, and error recovery
@Component({ /* ... */ })
export class SearchComponent implements OnInit, OnDestroy {
searchControl = new FormControl('');
results$!: Observable<SearchResult[]>;
private destroy$ = new Subject<void>();
constructor(private searchService: SearchService) {}
ngOnInit() {
this.results$ = this.searchControl.valueChanges.pipe(
debounceTime(300), // Wait 300ms after last keystroke
distinctUntilChanged(), // Skip if value hasn't changed
filter(query => query.length >= 2), // Minimum 2 characters
switchMap(query => // Cancel previous request
this.searchService.search(query).pipe(
catchError(err => { // Recover from errors
console.error('Search failed:', err);
return of([]); // Return empty results on error
}),
),
),
takeUntil(this.destroy$), // Unsubscribe on destroy
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Operadores de Mapeo de Orden Superior
Elegir el operador de aplanamiento correcto es crítico. switchMap cancela el Observable interno anterior cuando llega un nuevo valor externo (ideal para búsqueda, autocompletado). mergeMap ejecuta Observables internos concurrentemente (ideal para subidas de archivos en paralelo). concatMap encola Observables internos secuencialmente (ideal para escrituras ordenadas). exhaustMap ignora nuevos valores externos mientras el Observable interno actual está activo (ideal para clics de botón de login para prevenir envíos duplicados).switchMap cancels the previous inner Observable when a new outer value arrives (ideal for search, autocomplete). mergeMap runs inner Observables concurrently (ideal for parallel file uploads). concatMap queues inner Observables sequentially (ideal for ordered writes). exhaustMap ignores new outer values while the current inner Observable is active (ideal for login button clicks to prevent duplicate submissions).
Subjects y Gestión de Estado
Para estado a nivel de componente, BehaviorSubject (emite el valor actual a nuevos suscriptores) y ReplaySubject (reproduce N valores anteriores) son los caballos de batalla. Para estado a nivel de aplicación, NgRx provee un store estilo Redux con actions, reducers, effects (para efectos secundarios) y selectors (consultas memorizadas). El flujo unidireccional estricto de NgRx hace que las transiciones de estado complejas sean predecibles y depurables.BehaviorSubject (emits current value to new subscribers) and ReplaySubject (replays N previous values) are the workhorses. For application-level state, NgRx provides a Redux-like store with actions, reducers, effects (for side effects), and selectors (memoized queries). NgRx's strict unidirectional data flow makes complex state transitions predictable and debuggable.
// NgRx: typed actions, reducer, and selectors
// actions
export const loadMembers = createAction('[Members] Load', props<{ filters: MemberFilters }>());
export const loadMembersSuccess = createAction('[Members] Load Success', props<{ members: Member[] }>());
export const loadMembersFailure = createAction('[Members] Load Failure', props<{ error: string }>());
// reducer
export const membersReducer = createReducer(
initialState,
on(loadMembers, (state) => ({ ...state, loading: true, error: null })),
on(loadMembersSuccess, (state, { members }) => ({ ...state, members, loading: false })),
on(loadMembersFailure, (state, { error }) => ({ ...state, error, loading: false })),
);
// selectors
export const selectMembers = createFeatureSelector<MembersState>('members');
export const selectActiveMembers = createSelector(
selectMembers,
(state) => state.members.filter(m => m.status === 'active'),
);
// effect: side effect handling
loadMembers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadMembers),
switchMap(({ filters }) =>
this.memberService.getMembers(filters).pipe(
map(members => loadMembersSuccess({ members })),
catchError(error => of(loadMembersFailure({ error: error.message }))),
),
),
),
);
Errores Comunes con RxJS
El bug más común en Angular son los memory leaks por Observables no desuscritos. Cada suscripción en un componente debe limpiarse en ngOnDestroy. Usa el patrón takeUntil con un subject de destrucción, o usa el async pipe en templates que maneja el ciclo de vida de la suscripción automáticamente. Otro error: usar subscribe dentro de subscribe (suscripciones anidadas) en lugar de componer con switchMap, mergeMap o concatMap.ngOnDestroy. Use the takeUntil pattern with a destroy subject, or use the async pipe in templates which handles subscription lifecycle automatically. Another pitfall: using subscribe inside subscribe (nested subscriptions) instead of composing with switchMap, mergeMap, or concatMap.
3. Lazy Loading, Route Guards y Resolvers
El lazy loading divide la aplicación en chunks cargados bajo demanda cuando el usuario navega a una ruta. Cada feature module se convierte en un bundle JavaScript separado. El router dispara la descarga cuando la ruta se accede por primera vez. Esto reduce el bundle inicial de megabytes a kilobytes para el shell, con los bundles de features cargándose transparentemente.
// Lazy-loaded routes with guards and resolvers
const routes: Routes = [
{ path: '', component: DashboardComponent },
{
path: 'members',
loadChildren: () => import('./members/members.module').then(m => m.MembersModule),
canActivate: [AuthGuard],
canMatch: [AuthGuard], // Replaced canLoad in Angular 15.1+
data: { roles: ['admin', 'staff'] },
},
{
path: 'billing',
loadChildren: () => import('./billing/billing.module').then(m => m.BillingModule),
canActivate: [AuthGuard, RoleGuard],
data: { roles: ['admin'] },
},
{
path: 'reports',
loadComponent: () => import('./reports/reports.component').then(c => c.ReportsComponent),
canActivate: [AuthGuard],
resolve: { reportData: reportResolver },
},
];
Route Guards: canActivate, canMatch, canDeactivate
canActivate se ejecuta antes de que una ruta se active (se crea el componente). canMatch (que reemplazó a canLoad en Angular 15.1) se ejecuta antes de que el módulo lazy se descargue, previniendo requests de red innecesarios. canDeactivate se ejecuta al salir de una ruta, útil para diálogos de confirmación de “cambios sin guardar”. Los guards retornan boolean, UrlTree (redirección), u Observable
// Functional guard (Angular 14+)
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isAuthenticated$.pipe(
take(1),
map(isAuth => {
if (!isAuth) return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
const requiredRoles = route.data['roles'] as string[];
if (requiredRoles) {
const userRole = authService.currentUserRole;
if (!requiredRoles.includes(userRole)) {
return router.createUrlTree(['/unauthorized']);
}
}
return true;
}),
);
};
Route Resolvers
Los resolvers precargan datos antes de que una ruta se active, asegurando que el componente reciba datos completos en la inicialización. Con resolvers funcionales (Angular 15+), un resolver es una función simple que retorna un Observable, Promise o valor sincrónico. Los datos resueltos están disponibles vía ActivatedRoute.data. Los resolvers previenen el parpadeo de estado vacío y centralizan la lógica de carga de datos fuera de los componentes.Observable, Promise, or synchronous value. The resolved data is available via ActivatedRoute.data. Resolvers prevent empty-state flicker and centralize data-loading logic outside of components.
// Functional resolver (Angular 15+)
export const memberResolver: ResolveFn<Member> = (route) => {
const memberService = inject(MemberService);
const router = inject(Router);
const id = route.paramMap.get('id')!;
return memberService.getMember(id).pipe(
catchError(() => {
router.navigate(['/members']);
return EMPTY;
}),
);
};
// Usage in route config
{
path: 'members/:id',
component: MemberDetailComponent,
resolve: { member: memberResolver },
}
// Accessing resolved data in the component
@Component({ /* ... */ })
export class MemberDetailComponent {
member = inject(ActivatedRoute).data.pipe(map(d => d['member'] as Member));
}
Estrategias de Precarga
Angular provee estrategias de precarga que descargan módulos lazy en segundo plano después de la carga inicial. PreloadAllModules descarga todo. Las estrategias personalizadas pueden precargar basadas en rol de usuario, prioridad de ruta o condiciones de red. En producción, precargábamos el módulo de Members inmediatamente (el más accedido) mientras diferíamos Reports y Billing hasta idle.PreloadAllModules downloads everything. Custom strategies can preload based on user role, route priority, or network conditions. In production, we preloaded the Members module immediately (most accessed) while deferring Reports and Billing until idle.
4. Formularios: Reactivos vs Template-Driven
Angular ofrece dos enfoques de formularios. Los template-driven usan directivas (ngModel) y son adecuados para formularios simples. Los reactivos usan FormGroup/FormControl en la clase del componente, proveyendo seguridad de tipos, testeabilidad y control dinámico. Para aplicaciones empresariales, los formularios reactivos son la elección estándar.ngModel) and are suitable for simple forms. Reactive forms use FormGroup/FormControl in the component class, providing type safety, testability, and dynamic control. For enterprise applications, reactive forms are the standard choice.
Formularios Reactivos Tipados (Angular 14+)
// Strictly typed reactive form
interface MemberForm {
personal: FormGroup<{
name: FormControl<string>;
email: FormControl<string>;
phone: FormControl<string | null>;
}>;
subscription: FormGroup<{
planId: FormControl<string>;
startDate: FormControl<string>;
autoRenew: FormControl<boolean>;
}>;
emergencyContact: FormGroup<{
name: FormControl<string>;
phone: FormControl<string>;
}>;
}
@Component({ /* ... */ })
export class MemberFormComponent implements OnInit {
form!: FormGroup<MemberForm>;
constructor(private fb: NonNullableFormBuilder) {}
ngOnInit() {
this.form = this.fb.group({
personal: this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
phone: [null as string | null],
}),
subscription: this.fb.group({
planId: ['', Validators.required],
startDate: [new Date().toISOString().split('T')[0], Validators.required],
autoRenew: [true],
}),
emergencyContact: this.fb.group({
name: ['', Validators.required],
phone: ['', Validators.required],
}),
});
}
onSubmit() {
if (this.form.invalid) {
this.form.markAllAsTouched(); // Show all validation errors
return;
}
// form.getRawValue() is fully typed: MemberForm value shape
const value = this.form.getRawValue();
this.memberService.create(value).subscribe();
}
}
Validadores Personalizados y Validación Asíncrona
Los validadores personalizados son funciones puras que reciben un FormControl y retornan null (válido) o un objeto de error. Los validadores asíncronos hacen llamadas HTTP para validar contra el servidor (ej., verificar unicidad de email). Retornan Observable
// Async validator: check email uniqueness
function uniqueEmail(memberService: MemberService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) return of(null);
return timer(500).pipe( // 500ms debounce
switchMap(() => memberService.checkEmailExists(control.value)),
map(exists => exists ? { emailTaken: true } : null),
catchError(() => of(null)), // On error, consider valid
);
};
}
5. Testing de Aplicaciones Angular
Unit Testing con Jasmine y TestBed
El TestBed de Angular configura un módulo de testing que refleja el módulo de producción. Crea componentes con sus dependencias, habilitando verdaderos unit tests. Para servicios, usa TestBed.inject(). Para componentes, usa ComponentFixture para acceder al DOM, disparar detección de cambios y simular interacción del usuario. Karma sirve como el test runner por defecto, ejecutando specs de Jasmine en navegadores reales.TestBed configures a testing module that mirrors the production module. It creates components with their dependencies, enabling true unit tests. For services, use TestBed.inject(). For components, use ComponentFixture to access the DOM, trigger change detection, and simulate user interaction. Karma serves as the default test runner, executing Jasmine specs in real browsers.
// Component test with TestBed
describe('MemberListComponent', () => {
let component: MemberListComponent;
let fixture: ComponentFixture<MemberListComponent>;
let memberService: jasmine.SpyObj<MemberService>;
beforeEach(async () => {
memberService = jasmine.createSpyObj('MemberService', ['getMembers', 'deleteMember']);
memberService.getMembers.and.returnValue(of(mockMembers));
await TestBed.configureTestingModule({
declarations: [MemberListComponent, MemberStatusPipe],
imports: [RouterTestingModule],
providers: [
{ provide: MemberService, useValue: memberService },
],
}).compileComponents();
fixture = TestBed.createComponent(MemberListComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // triggers ngOnInit
});
it('should display member count', () => {
const heading = fixture.nativeElement.querySelector('h2');
expect(heading.textContent).toContain(`${mockMembers.length} Members`);
});
it('should call delete and refresh list', () => {
memberService.deleteMember.and.returnValue(of(void 0));
memberService.getMembers.and.returnValue(of(mockMembers.slice(1)));
component.onDelete(mockMembers[0].id);
fixture.detectChanges();
expect(memberService.deleteMember).toHaveBeenCalledWith(mockMembers[0].id);
expect(memberService.getMembers).toHaveBeenCalledTimes(2); // initial + refresh
});
});
Testeando Servicios con HTTP
Usa HttpClientTestingModule y HttpTestingController para testear servicios que hacen requests HTTP. El testing controller intercepta requests, permitiéndote verificar la URL, método, headers y body, y luego emitir una respuesta mock. Esto testea la lógica completa del servicio incluyendo manejo de errores y transformación de respuestas.HttpClientTestingModule and HttpTestingController to test services that make HTTP requests. The testing controller intercepts requests, allowing you to verify the URL, method, headers, and body, then flush a mock response. This tests the full service logic including error handling and response transformation.
// Service test with HttpTestingController
describe('MemberService', () => {
let service: MemberService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MemberService],
});
service = TestBed.inject(MemberService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify()); // Ensure no outstanding requests
it('should fetch members with filters', () => {
const mockMembers: Member[] = [{ id: '1', name: 'Ana', status: 'active' }];
service.getMembers({ status: 'active' }).subscribe(members => {
expect(members).toEqual(mockMembers);
});
const req = httpMock.expectOne('/api/members?status=active');
expect(req.request.method).toBe('GET');
req.flush(mockMembers);
});
});
Testing E2E: De Protractor a Cypress
Protractor (la herramienta E2E original de Angular) fue deprecada en Angular 12. La comunidad migró a Cypress o Playwright. Cypress ejecuta tests en el mismo loop del navegador, proveyendo ejecución más rápida, esperas automáticas y depuración con viaje en el tiempo. Para la plataforma, migrar de Protractor a Cypress redujo el tiempo de ejecución de tests E2E de 12 minutos a 3 minutos y eliminó tests inestables causados por esperas manuales.
// Cypress E2E test for member management
describe('Member Management', () => {
beforeEach(() => {
cy.login('[email protected]', 'your-password'); // Custom command
cy.intercept('GET', '/api/members*', { fixture: 'members.json' }).as('getMembers');
cy.visit('/members');
cy.wait('@getMembers');
});
it('should filter members by status', () => {
cy.get('[data-cy="status-filter"]').select('active');
cy.wait('@getMembers');
cy.get('[data-cy="member-row"]').should('have.length', 5);
cy.get('[data-cy="member-status"]').each($el => {
expect($el.text().trim()).to.equal('Active');
});
});
it('should create a new member', () => {
cy.intercept('POST', '/api/members', { statusCode: 201, body: { id: '99' } }).as('create');
cy.get('[data-cy="add-member"]').click();
cy.get('[data-cy="name-input"]').type('Carlos Lopez');
cy.get('[data-cy="email-input"]').type('[email protected]');
cy.get('[data-cy="submit"]').click();
cy.wait('@create').its('request.body').should('include', { name: 'Carlos Lopez' });
});
});
6. Angular CLI y Schematics
El Angular CLI es más que una herramienta de scaffolding. Gestiona todo el ciclo de vida del desarrollo: generar componentes, compilar para producción, ejecutar tests, linting y despliegue. Internamente usa webpack (o esbuild en versiones recientes) con configuraciones optimizadas. El comando ng update automatiza actualizaciones de versión de Angular, ejecutando schematics de migración que transforman código y configuración.ng update command automates Angular version upgrades, running migration schematics that transform code and configuration.
Schematics Personalizados
Los schematics son generadores y transformadores de código. Los schematics personalizados refuerzan convenciones del equipo generando componentes con la estructura, imports y boilerplate correctos. También pueden realizar transformaciones estilo codemod a través del proyecto. Para equipos empresariales, los schematics reemplazan la revisión manual de código para patrones de boilerplate.
// Custom schematic: generate a feature module with CRUD components
// Usage: ng generate @myorg/schematics:feature --name=members --entity=Member
// Generates:
// members/
// members.module.ts
// members-routing.module.ts
// components/
// member-list/member-list.component.ts
// member-detail/member-detail.component.ts
// member-form/member-form.component.ts
// services/
// member.service.ts
// models/
// member.model.ts
// store/ (if NgRx flag is set)
// member.actions.ts
// member.reducer.ts
// member.effects.ts
// member.selectors.ts
Custom Builders y Optimización de Build
Los builders controlan el pipeline de build. El builder @angular-devkit/build-angular:application (Angular 17+) usa esbuild y Vite para builds dramáticamente más rápidos. Los builders personalizados pueden agregar pasos como optimización de SVG, generación de documentación de API o procesamiento de assets específico por entorno. La configuración angular.json del workspace controla budgets, reemplazos de archivos y flags de optimización por build target.@angular-devkit/build-angular:application builder (Angular 17+) uses esbuild and Vite for dramatically faster builds. Custom builders can add steps like SVG optimization, API documentation generation, or environment-specific asset processing. The angular.json workspace configuration controls budgets, file replacements, and optimization flags per build target.
7. Signals y Primitivas Reactivas
Angular Signals (introducidos en Angular 16) proveen un modelo de reactividad sincrónico y de grano fino. A diferencia de los Observables de RxJS, los signals son sincrónicos, siempre tienen un valor actual y se integran directamente con la detección de cambios de Angular. El framework puede rastrear qué signals lee un componente y re-renderizar solo cuando esos valores específicos cambian, habilitando detección de cambios sin zones.
// Signals: reactive state without RxJS boilerplate
@Component({
selector: 'app-member-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<h2>{{ title() }}</h2>
<p>Total: {{ memberCount() }} | Active: {{ activeCount() }}</p>
<input [value]="filter()" (input)="filter.set($any($event.target).value)" />
@for (member of filteredMembers(); track member.id) {
<div>{{ member.name }} - {{ member.status }}</div>
}
`,
})
export class MemberDashboardComponent {
private memberService = inject(MemberService);
// Writable signals
title = signal('Member Dashboard');
filter = signal('');
members = signal<Member[]>([]);
// Computed signals: derived state, recalculated only when dependencies change
memberCount = computed(() => this.members().length);
activeCount = computed(() => this.members().filter(m => m.status === 'active').length);
filteredMembers = computed(() => {
const term = this.filter().toLowerCase();
return this.members().filter(m => m.name.toLowerCase().includes(term));
});
constructor() {
// effect: run side effects when signals change
effect(() => {
console.log(`Filter changed to: ${this.filter()}`);
});
}
}
// Signal interop with RxJS
// Convert Observable to signal
const members = toSignal(this.memberService.getMembers(), { initialValue: [] });
// Convert signal to Observable
const filter$ = toObservable(this.filter);
Inputs y Queries Basados en Signals (Angular 17.1+)
Los signal inputs reemplazan el decorador @Input() con una API basada en signals. Son signals de solo lectura que rastrean valores de input reactivamente. Combinados con las queries de signals viewChild y contentChildren, habilitan un ciclo de vida de componente completamente dirigido por signals sin hooks manuales de detección de cambios.@Input() decorator with a signal-based API. They are read-only signals that track input values reactively. Combined with viewChild and contentChildren signal queries, they enable a fully signal-driven component lifecycle without manual change detection hooks.
// Signal-based inputs and queries
@Component({
selector: 'app-member-card',
standalone: true,
template: `
<div class="card" #cardEl>
<h3>{{ name() }}</h3>
<p>Status: {{ status() }}</p>
<p>Display: {{ displayName() }}</p>
</div>
`,
})
export class MemberCardComponent {
// Signal inputs: read-only signals updated by parent
name = input.required<string>();
status = input<string>('active');
// Computed from signal inputs
displayName = computed(() => `${this.name()} (${this.status()})`);
// Signal query: reactive reference to template element
cardEl = viewChild.required<ElementRef>('cardEl');
}
Angular 20/21: Signals Estables y Detección de Cambios Zoneless
Angular 20 (mayo 2025) hace los signals completamente estables: signal(), computed(), effect(), linkedSignal(), inputs basados en signals y queries de signals están listos para producción. La detección de cambios zoneless se estabilizó en Angular 20.2, entregando significativamente más velocidad en el rendering al eliminar el overhead de Zone.js. La hidratación incremental para SSR también es estable. Las nuevas APIs experimentales incluyen resource() y httpResource() para carga de datos asíncrona declarativa.signal(), computed(), effect(), linkedSignal(), signal-based inputs, and signal queries are all production-ready. Zoneless change detection became stable in Angular 20.2, delivering significantly faster rendering by removing Zone.js overhead. Incremental hydration for SSR is also stable. New experimental APIs include resource() and httpResource() for declarative async data loading.
// Angular 20: linkedSignal - writable derived signals
@Component({
selector: 'app-member-filter',
standalone: true,
template: `
<select (change)="selectedPlan.set($any($event.target).value)">
@for (plan of plans(); track plan) {
<option [value]="plan">{{ plan }}</option>
}
</select>
<p>Selected: {{ selectedPlan() }}</p>
`,
})
export class MemberFilterComponent {
plans = signal(['Basic', 'Premium', 'VIP']);
// linkedSignal: derived from plans(), resets when plans change,
// but is also writable (user can override)
selectedPlan = linkedSignal(() => this.plans()[0]);
}
// Angular 20: resource() for declarative async data
@Component({
selector: 'app-member-detail',
standalone: true,
template: `
@if (memberResource.isLoading()) {
<p>Loading...</p>
} @else {
<h2>{{ memberResource.value()?.name }}</h2>
}
`,
})
export class MemberDetailComponent {
memberId = input.required<string>();
private http = inject(HttpClient);
memberResource = resource({
request: () => this.memberId(),
loader: ({ request: id }) => this.http.get<Member>(`/api/members/${id}`),
});
}
Migración Zoneless: Eliminando Zone.js
En Angular 21, la detección de cambios zoneless es el valor por defecto para proyectos nuevos. Eliminar Zone.js reduce el tamaño del bundle en ~30KB y elimina ciclos de detección de cambios innecesarios disparados por setTimeout, resolución de Promises o eventos DOM que no modifican estado. Los Signal Forms, también introducidos en Angular 21, proveen formularios dinámicos reactivos construidos completamente sobre signals. El Template HMR (Hot Module Replacement) es estable, habilitando actualizaciones instantáneas de templates durante el desarrollo sin perder el estado del componente.default for new projects. Removing Zone.js reduces bundle size by ~30KB and eliminates unnecessary change detection cycles triggered by setTimeout, Promise resolution, or DOM events that do not modify state. Signal Forms, also introduced in Angular 21, provide reactive dynamic forms built entirely on signals. Template HMR (Hot Module Replacement) is stable, enabling instant template-only updates during development without losing component state.
// Angular 21: Zoneless is the default. For existing apps, opt in:
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(), // Remove Zone.js dependency
provideRouter(routes),
provideHttpClient(),
],
});
// Ensure components use signals or markForCheck() for change detection
// Zone.js previously triggered CD on every async event; without it,
// only signal reads and explicit markForCheck() trigger re-renders.
// Angular 21: Signal Forms (reactive forms built on signals)
const name = formSignal('');
const email = formSignal('', { validators: [Validators.required, Validators.email] });
// Reactive: automatically tracks validity
const isValid = computed(() => name.valid() && email.valid());
8. Renderizado del Lado del Servidor con Angular Universal
Angular Universal renderiza aplicaciones Angular en el servidor, produciendo HTML estático que se envía al navegador antes de que el bundle JavaScript cargue. Esto mejora el First Contentful Paint (FCP), habilita el rastreo de motores de búsqueda de contenido dinámico y provee una mejor experiencia en redes lentas. A partir de Angular 17, el soporte de SSR está integrado en el framework vía el paquete @angular/ssr.@angular/ssr package.
// Adding SSR to an Angular project
// Angular 17+: built-in SSR support
ng new my-app --ssr
// Or add SSR to existing project
ng add @angular/ssr
// server.ts: Express server for SSR
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
const app = express();
const commonEngine = new CommonEngine();
app.get('*', (req, res, next) => {
commonEngine.render({
bootstrap,
documentFilePath: indexHtml,
url: req.originalUrl,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
})
.then(html => res.send(html))
.catch(err => next(err));
});
Hidratación y Transfer State
La hidratación completa (Angular 16+) reutiliza el DOM renderizado en el servidor en lugar de destruirlo y recrearlo. La función provideClientHydration() habilita este comportamiento. TransferState previene requests HTTP duplicados transfiriendo datos obtenidos en el servidor al cliente. Sin transfer state, la aplicación obtiene datos dos veces: una en el servidor y otra después de la hidratación en el cliente.provideClientHydration() function enables this behavior. TransferState prevents duplicate HTTP requests by transferring server-fetched data to the client. Without transfer state, the application fetches data twice: once on the server and once after hydration on the client.
// Enable hydration and transfer state
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()),
provideClientHydration(withHttpTransferCacheOptions({
includePostRequests: false,
})),
],
};
// Platform-aware service: skip browser-only APIs on the server
@Injectable({ providedIn: 'root' })
export class StorageService {
private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
get(key: string): string | null {
return this.isBrowser ? localStorage.getItem(key) : null;
}
set(key: string, value: string): void {
if (this.isBrowser) localStorage.setItem(key, value);
}
}
9. Internacionalización (i18n)
El sistema i18n integrado de Angular usa traducción en tiempo de compilación. Marcas texto traducible con el atributo i18n en templates, extraes mensajes con el CLI, los traduces y compilas bundles separados por locale. Este enfoque produce bundles optimizados sin overhead de traducción en runtime, pero requiere un build y despliegue separado por idioma.i18n attribute in templates, extract messages with the CLI, translate them, and build separate bundles per locale. This approach produces optimized bundles with no runtime translation overhead, but requires a separate build and deployment per language.
<!-- Template with i18n markers -->
<h1 i18n="page title|Title for the member list page">Member List</h1>
<p i18n="@@memberCount">{memberCount, plural,
=0 {No members found}
=1 {1 member found}
other {{{memberCount}} members found}
}</p>
<button i18n="@@deleteButton">Delete</button>
<!-- Extract messages -->
<!-- ng extract-i18n --output-path src/locale -->
<!-- angular.json: configure locale builds -->
<!-- "i18n": {
"sourceLocale": "en-US",
"locales": {
"es-CO": "src/locale/messages.es-CO.xlf"
}
} -->
Traducción en Runtime con Transloco/ngx-translate
Para cambio de idioma en runtime sin builds separados, librerías como @jsverse/transloco (sucesora de ngx-translate) cargan archivos de traducción bajo demanda. Esto intercambia un pequeño overhead en runtime por simplicidad de despliegue: un solo build sirve todos los idiomas. i18n en runtime es la elección pragmática cuando necesitas cambio instantáneo de idioma en SPAs o cuando build-por-locale es impráctico.@jsverse/transloco (successor to ngx-translate) load translation files on demand. This trades a small runtime overhead for deployment simplicity: a single build serves all languages. Runtime i18n is the pragmatic choice when you need instant language switching in SPAs or when build-per-locale is impractical.
// Transloco: runtime i18n
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideTransloco({
config: {
availableLangs: ['en', 'es'],
defaultLang: 'en',
reRenderOnLangChange: true,
prodMode: !isDevMode(),
},
loader: TranslocoHttpLoader,
}),
],
};
// Template usage
// <h1>{{ 'memberList.title' | transloco }}</h1>
// <p>{{ 'memberList.count' | transloco: { count: memberCount } }}</p>
// Programmatic language switch
translocoService.setActiveLang('es');
10. Estrategias de Migración
Migración de AngularJS a Angular
Migrar de AngularJS (1.x) a Angular (2+) es un emprendimiento mayor. El módulo ngUpgrade permite ejecutar ambos frameworks simultáneamente, habilitando migración incremental. La estrategia: configurar una aplicación híbrida, migrar servicios primero (no tienen dependencias de UI), luego migrar componentes de abajo hacia arriba (componentes hoja antes que contenedores), y finalmente eliminar el bootstrap de AngularJS.ngUpgrade module enables running both frameworks simultaneously, allowing incremental migration. The strategy: set up a hybrid application, migrate services first (they have no UI dependencies), then migrate components bottom-up (leaf components before containers), and finally remove the AngularJS bootstrap.
Actualizaciones de Versión de Angular
Angular sigue versionado semántico con un release mayor cada 6 meses. El comando ng update maneja la mayoría de cambios breaking automáticamente. Para saltos de múltiples versiones (ej., v5 a v17), actualiza una versión mayor a la vez. Cada versión tiene schematics de migración que manejan APIs renombradas, estructuras de módulos cambiadas y features deprecadas.ng update command handles most breaking changes automatically. For multi-version jumps (e.g., v5 to v17), upgrade one major version at a time. Each version has migration schematics that handle renamed APIs, changed module structures, and deprecated features.
# Incremental Angular upgrade path (v5 to v17)
# Each step: update, run schematics, fix any remaining issues, test
ng update @angular/core@6 @angular/cli@6 # RxJS 6 migration, HttpClient
ng update @angular/core@7 @angular/cli@7 # Virtual scrolling, drag & drop
ng update @angular/core@8 @angular/cli@8 # Differential loading, Ivy preview
ng update @angular/core@9 @angular/cli@9 # Ivy default, TestBed changes
ng update @angular/core@10 @angular/cli@10 # Strict mode option, warnings
ng update @angular/core@11 @angular/cli@11 # Stricter types, webpack 5 preview
ng update @angular/core@12 @angular/cli@12 # Webpack 5, strict by default, Ivy everywhere
ng update @angular/core@13 @angular/cli@13 # View Engine removed, Node 16
ng update @angular/core@14 @angular/cli@14 # Standalone components, typed forms
ng update @angular/core@15 @angular/cli@15 # Standalone APIs stable, directive composition
ng update @angular/core@16 @angular/cli@16 # Signals, required inputs, esbuild dev server
ng update @angular/core@17 @angular/cli@17 # New control flow, SSR built-in, Vite default
ng update @angular/core@18 @angular/cli@18 # Zoneless preview, @let syntax, route redirects as functions
ng update @angular/core@19 @angular/cli@19 # Standalone default, linkedSignal, resource API
ng update @angular/core@20 @angular/cli@20 # Signals stable, zoneless stable (20.2), incremental hydration
ng update @angular/core@21 @angular/cli@21 # Zoneless default, Signal Forms, template HMR stable
Migración a Standalone Components
Angular provee schematics automatizados para migrar aplicaciones basadas en NgModules a standalone components. El schematic ng generate @angular/core:standalone convierte componentes, actualiza sus imports y reconfigura el routing. Esta migración puede hacerse incrementalmente: standalone y componentes basados en NgModule coexisten en la misma aplicación.ng generate @angular/core:standalone schematic converts components, updates their imports, and rewires the routing configuration. This migration can be done incrementally: standalone and NgModule-based components coexist in the same application.
# Automated migration to standalone components
ng generate @angular/core:standalone
# Step 1: Convert declarations to standalone
# Step 2: Remove unnecessary NgModules
# Step 3: Switch to standalone bootstrap via bootstrapApplication()
Últimas Actualizaciones (Julio 2026)
Angular 22.0.8 (22 de Julio, 2026)
Angular 22.0.0 se lanzó el 3 de junio de 2026, y la línea ya llegó a 22.0.8 (22 de julio de 2026), con 22.1.0 en release candidate. La línea 21.x continúa recibiendo parches de mantenimiento (21.2.18, 8 de julio) con soporte hasta el 19 de mayo de 2027, y 20.3.x (20.3.26) permanece en soporte a largo plazo. Los equipos en 21.x deberían planear la actualización con ng update; la migración es incremental y el release 22 no contiene breaking changes mayores para apps que ya usan zoneless y signals.ng update; the migration is incremental and the 22 release contains no major breaking changes for apps already on zoneless and signals.
Angular 22 Lanzado (3 de Junio, 2026)
Angular 22 estabiliza la base signal-first: Signal Forms, las APIs asíncronas de recursos (resource(), httpResource(), rxResource()) y Angular Aria (patrones de componentes headless accesibles) son todos estables. La detección de cambios OnPush ahora es el valor por defecto para componentes recién generados. Los componentes sin selector (selectorless components) -- importar componentes en templates por referencia en vez de selectores string -- siguen en desarrollo activo, con los primeros pre-releases disponibles en builds next pero aún no estables en v22. El release también expande las herramientas de IA de Angular: Agent Skills, soporte WebMCP y el Angular CLI MCP Server para desarrollo asistido por IA.Signal Forms, the async resource APIs (resource(), httpResource(), rxResource()), and Angular Aria (accessible headless component patterns) are all stable. OnPush change detection is now the default for newly generated components. Selectorless components -- importing components into templates by reference instead of string selectors -- remain in active development, with first pre-releases available in next builds but not yet stable in v22. The release also expands Angular's AI tooling: Agent Skills, WebMCP support, and the Angular CLI MCP Server for AI-assisted development.
Angular 20: Sintaxis Moderna de Templates y Verificación de Tipos en Host Bindings
Angular 20 (estable desde mayo 2025, ahora en 20.3.x) introdujo sintaxis moderna de templates incluyendo template string literals, el operador de exponenciación (**), la palabra clave in y el operador void directamente en templates. La verificación de tipos en host bindings ahora valida cada expresión en los metadatos host del componente y las anotaciones @HostBinding/@HostListener en tiempo de compilación, detectando errores tipográficos y discrepancias de tipos antes del runtime. Los early adopters reportan renders iniciales 30-40% más rápidos y una reducción del 50% en re-renders innecesarios comparado con Angular 19.modern template syntax including template string literals, the exponentiation operator (**), the in keyword, and the void operator directly in templates. Host binding type checking now validates every expression in a component's host metadata and @HostBinding/@HostListener annotations at compile time, catching typos and type mismatches before runtime. Early adopters report 30-40% faster initial renders and a 50% reduction in unnecessary re-renders compared to Angular 19.
Angular 20: Hidratación Incremental Estable y Detección de Cambios sin Zone.js
Angular 20 graduó la hidratación incremental de developer preview a estable. Los componentes ahora pueden hidratarse bajo demanda usando triggers como interacción del usuario, visibilidad en el viewport o señales personalizadas -- reduciendo drásticamente el Time to Interactive en páginas SSR. La detección de cambios sin Zone.js también se estabilizó, eliminando la dependencia de Zone.js por completo. Combinado con signal(), effect(), linkedSignal(), queries basadas en signals e inputs basados en signals estables, Angular 20 marca la transición a un modelo de reactividad completamente basado en signals. El resultado es rendimiento más predecible, bundles más pequeños (sin polyfill de Zone.js) y código de componentes más limpio.incremental hydration from developer preview to stable. Components can now hydrate on demand using triggers like user interaction, viewport visibility, or custom signals -- dramatically reducing Time to Interactive on SSR pages. Zoneless change detection also stabilized, eliminating the Zone.js dependency entirely. Combined with stable signal(), effect(), linkedSignal(), signal-based queries, and signal-based inputs, Angular 20 marks the transition to a fully signal-driven reactivity model. The result is more predictable performance, smaller bundles (no Zone.js polyfill), and cleaner component code.
Signal Forms y la Era Signal-First
Signal Forms, introducidos como experimentales en Angular 21, alcanzaron estado estable en Angular 22 (junio 2026). A diferencia de Reactive Forms que reevalúan todo el árbol del formulario ante cualquier cambio, Signal Forms proveen reactividad granular: cambiar un campo en un formulario de 50 campos solo actualiza ese campo específico. Angular 22 también hizo de OnPush la detección de cambios por defecto para componentes nuevos, elevó los mínimos del toolchain (versiones más recientes de TypeScript y Node.js), y continuó la inversión en el Angular MCP Server para herramientas de desarrollo asistidas por IA.OnPush change detection the default for new components, raised the toolchain floor (newer TypeScript and Node.js minimums), and continued investment in the Angular MCP Server for AI-assisted development tooling.