From Angular 16 to 19: A Complete Migration Journey with Real-World Challenges
A comprehensive guide to upgrading your Angular application across multiple major versions while navigating breaking changes, deprecated…
From Angular 16 to 19: A Complete Migration Journey with Real-World Challenges
A comprehensive guide to upgrading your Angular application across multiple major versions while navigating breaking changes, deprecated libraries, and evolving best practices.
Introduction
Upgrading a production Angular application across multiple major versions is rarely straightforward. When security vulnerabilities force your hand, the pressure intensifies. This blog documents our complete journey migrating an enterprise Angular application from version 16 to 19, including every challenge, workaround, and lesson learned along the way.
Whether you’re facing a similar multi-version upgrade or just planning your next Angular update, this guide will help you anticipate obstacles and navigate them successfully.
The Catalyst: Security Vulnerabilities
Our upgrade journey began not with a desire for new features, but with security audit findings. Running **npm audit** revealed multiple vulnerabilities in our Node.js and Angular dependency chain:
┌───────────────────────────────────────────────────────────────────┐
│ npm audit report │
├───────────────────────────────────────────────────────────────────┤
│ High │ Prototype Pollution in @angular-devkit/build-angular │
│ High │ ReDoS vulnerability in zone.js │
│ Medium │ Memory exposure in older TypeScript versions │
└───────────────────────────────────────────────────────────────────┘
Additionally, our application used Nebular, a UI component library that had been deprecated and was no longer receiving security updates. The combination of framework vulnerabilities and an unmaintained UI library made upgrading non-negotiable.
Key Insight
Don’t wait for security audits to force your hand. Regular dependency updates are cheaper than emergency migrations.
Planning the Migration Strategy
Incremental vs. Direct Upgrade
Angular officially recommends upgrading one major version at a time. While it might be tempting to jump directly from Angular 16 to 19, this approach has significant risks:

We chose the incremental approach for several reasons:
- Each version has specific migration schematics that automate common changes
- Breaking changes are easier to identify and fix in isolation
- If something breaks, you know exactly which version caused it
Our Migration Roadmap
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: Replace Nebular with Angular Material + PrimeNG │
│ Step 2: Upgrade Angular 16 → 17 │
│ Step 3: Upgrade Angular 17 → 18 │
│ Step 4: Upgrade Angular 18 → 19 │
│ Step 5: Update Dockerfile to Node 22 │
│ Step 6: Fix PrimeNG 19 breaking changes │
└─────────────────────────────────────────────────────────────────┘
Step 1: Replacing Nebular with Modern Alternatives
Why Nebular Had to Go
Nebular was an excellent UI library for its time, but:
- No longer actively maintained
- Tight coupling with specific Angular versions
- Security vulnerabilities with no patches forthcoming
Choosing Replacements
We mapped Nebular components to modern alternatives:

Migration Code Example
Before (Nebular):
import { NbLayoutModule, NbSidebarModule, NbMenuModule } from '@nebular/theme';
@NgModule({
imports: [
NbLayoutModule,
NbSidebarModule.forRoot(),
NbMenuModule.forRoot(),
]
})
After (Material + PrimeNG):
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { TableModule } from 'primeng/table';
@NgModule({
imports: [
MatToolbarModule,
MatSidenavModule,
MatListModule,
TableModule,
]
})
Key Challenge: Template Migration
Nebular’s template syntax differs significantly from Material:
Nebular Layout:
<nb-layout>
<nb-layout-header>Header</nb-layout-header>
<nb-sidebar>Sidebar</nb-sidebar>
<nb-layout-column>Content</nb-layout-column>
</nb-layout>
Angular Material Layout:
<mat-sidenav-container>
<mat-sidenav mode="side" opened>Sidebar</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar>Header</mat-toolbar>
<main>Content</main>
</mat-sidenav-content>
</mat-sidenav-container>
Step 2: Angular 16 → 17
Running the Update
ng update @angular/core@17 @angular/cli@17
Challenge 1: TypeScript Version Requirements
Angular 17 requires TypeScript 5.2+. Our project was on TypeScript 4.9:
npm install typescript@~5.2.0
Challenge 2: Zone.js Update
Angular 17 needs zone.js 0.14+:
npm install zone.js@~0.14.0
Angular 17 Key Changes
- Deferrable Views (
@defer): New syntax for lazy loading - Built-in Control Flow: New
@if,@for,@switchsyntax (optional) - View Transitions API: Native browser transitions support
We didn’t adopt the new control flow syntax immediately — the migration can be done incrementally.
Step 3: Angular 17 → 18
Running the Update
ng update @angular/core@18 @angular/cli@18
Challenge 1: Zone.js 0.15 Requirements
Angular 18 requires zone.js 0.15:
npm install zone.js@~0.15.0
Challenge 2: Material Component Updates
Angular Material 18 introduced changes to component APIs. Some Material components had updated input/output property names.
Angular 18 Key Changes
- Zoneless Change Detection (Experimental): Opt-in feature for better performance
- Stable Signals API: Reactive primitives are now stable
- New Build System: Esbuild-based application builder is the default
Step 4: Angular 18 → 19 (The Big One)
This was the most challenging upgrade, introducing several breaking changes that required significant code modifications.
Running the Update
ng update @angular/core@19 @angular/cli@19
Breaking Change 1: Standalone Components by Default
The Problem:
Angular 19 defaults all components to standalone: true. If you have module-based components declared in NgModule.declarations, they'll break.
The Error:
NG2007: Component 'MyComponent' is standalone and cannot be declared in an NgModule.
The Solution:
Add standalone: false to all your existing components:
@Component({
selector: 'app-my-component',
standalone: false, // 👈 Required for Angular 19 with NgModules
templateUrl: './my-component.component.html',
})
export class MyComponent { }
Automation Tip:
You can use find-and-replace across your project:
# Find all @Component decorators and add standalone: false
# This is a simplified example - regex may vary
Or use Angular’s schematic:
ng generate @angular/core:standalone --convert-to-declarations
Breaking Change 2: PrimeNG 19 Theming Overhaul
Angular 19 upgrade often coincides with PrimeNG 19 upgrade. PrimeNG 19 completely changed its theming system.
The Problem:
Old CSS imports no longer exist:
// ❌ This no longer works in PrimeNG 19
@import "primeng/resources/themes/lara-light-blue/theme.css";
@import "primeng/resources/primeng.css";
The Solution:
PrimeNG 19 uses a new programmatic theming approach:
// app.module.ts
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeuix/themes/aura';
@NgModule({
providers: [
providePrimeNG({
theme: {
preset: Aura,
options: {
darkModeSelector: 'none' // Force light mode
}
}
})
]
})
TypeScript Declaration Required:
If you get “Cannot find module ‘@primeuix/themes/aura’” errors, create a type declaration file:
// src/primeng-theme.d.ts
declare module '@primeuix/themes/aura' {
const Aura: any;
export default Aura;
}
Breaking Change 3: PrimeNG Table Template Syntax
The most frustrating change — PrimeNG 19 changed how table templates are referenced.
The Problem:
Row expansion stopped working. Clicking the toggle button did nothing.
Old Syntax (PrimeNG 18 and earlier):
<ng-template pTemplate="header">
<tr><th>Column</th></tr>
</ng-template>
<ng-template pTemplate="body" let-rowData>
<tr><td>{{ rowData.value }}</td></tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-details>
<tr><td>Expanded content</td></tr>
</ng-template>
New Syntax (PrimeNG 19):
<ng-template #header>
<tr><th>Column</th></tr>
</ng-template>
<ng-template #body let-rowData>
<tr><td>{{ rowData.value }}</td></tr>
</ng-template>
<ng-template #expandedrow let-details>
<tr><td>Expanded content</td></tr>
</ng-template>
Key Changes:
pTemplate="header"→#headerpTemplate="body"→#bodypTemplate="rowexpansion"→#expandedrow(note: NOT#rowexpansion!)
This was particularly tricky to debug because:
- No compilation errors
- No runtime errors
- The feature just silently stopped working
Debugging Tip: When PrimeNG features stop working after an upgrade, check their official documentation for the current syntax. Template naming conventions change between major versions.
Breaking Change 4: Two-Way Binding Strictness
Angular 19 is stricter about two-way binding with certain types.
The Problem:
TS2322: Type 'boolean' is not assignable to type '{ [key: string]: boolean }'
The Solution:
Use one-way binding with explicit event handlers:
<!-- Instead of this -->
<p-table [(expandedRowKeys)]="expandedRowKeys">
<!-- Use this -->
<p-table [expandedRowKeys]="expandedRowKeys"
(onRowExpand)="onRowExpand($event)"
(onRowCollapse)="onRowCollapse($event)">
Step 5: Dockerfile Updates for Node 22
Modern Angular requires a modern Node.js runtime. Node 22 provides:
- Latest V8 JavaScript engine optimizations
- Patched TLS/HTTP2/Buffer vulnerabilities
- Better ES Module support
Before:
FROM node:18-alpine AS build
After:
FROM node:22-alpine AS build
RUN apk update && apk upgrade --available && sync
Security Best Practice
Always update system packages in your Docker build to catch CVE patches:
RUN apk update && apk upgrade --available && rm -rf /var/cache/apk/*
Testing the Migration
Verification Checklist
After completing the migration, verify:
# 1. Build succeeds
npm run build
# 2. No critical audit vulnerabilities
npm audit
# 3. Unit tests pass
npm run test
# 4. E2E tests pass (if applicable)
npm run e2e
# 5. Manual testing of key features
# - Navigation works
# - Data tables render and expand
# - Forms submit correctly
# - Dropdowns and calendars function
Common Post-Migration Issues

Lessons Learned
1. Upgrade Incrementally
Each major version has specific migration schematics. Skipping versions means missing automated migrations.
2. Read the Changelogs
Both Angular and PrimeNG publish detailed migration guides. The PrimeNG 19 migration guide specifically mentions the template syntax changes — we found it after hours of debugging.
3. Test Interactively, Not Just Build
A successful ng build doesn't mean everything works. Interactive features like expandable rows can fail silently.
4. Type Declarations for Third-Party Themes
When third-party libraries update their module structure, you may need custom TypeScript declarations.
5. Dark Mode Can Surprise You
PrimeNG 19 respects system dark mode preferences by default. If you want light-only, explicitly set darkModeSelector: 'none'.
6. Git Branch Strategy
Create a dedicated branch for the upgrade. Commit after each successful version increment:
git checkout -b chore/ng19-upgrade
# After each step
git add -A && git commit -m "chore: upgrade to Angular 17"
git add -A && git commit -m "chore: upgrade to Angular 18"
git add -A && git commit -m "chore: upgrade to Angular 19"
Final Dependency Versions
After completing the migration:
{
"@angular/core": "^19.2.0",
"@angular/cli": "^19.2.0",
"@angular/material": "^19.2.0",
"primeng": "^19.1.0",
"@primeuix/themes": "^2.0.0",
"typescript": "~5.8.0",
"zone.js": "~0.15.0",
"rxjs": "~7.8.0"
}
Conclusion
Migrating from Angular 16 to 19 took careful planning and patience. The key challenges were:
- Replacing deprecated Nebular with Angular Material and PrimeNG
- Adding
standalone: falseto all NgModule-based components - Updating PrimeNG theme configuration to the new programmatic API
- Fixing PrimeNG table template syntax from
pTemplate=to#name - Updating Node.js to version 22 in Docker
Each challenge had a solution, but finding those solutions required research, experimentation, and occasionally consulting GitHub issues.
The investment is worth it: improved security, better performance, and access to modern Angular features like Signals, the new control flow syntax, and eventually zoneless change detection.
Resources
- Angular Update Guide
- Angular 19 Release Blog
- PrimeNG 19 Migration Guide
- PrimeNG Table Documentation
- Angular Material Getting Started
Have questions about your own Angular migration? Drop a comment below!
메타데이터
- post_id
- 43d9c4dc91f7
- slug
- from-angular-16-to-19-a-complete-migration-journey-with-real-world-challenges-43d9c4dc91f7
- url
- https://medium.com/@imkss/from-angular-16-to-19-a-complete-migration-journey-with-real-world-challenges-43d9c4dc91f7
- canonical_url
- https://medium.com/@imkss/from-angular-16-to-19-a-complete-migration-journey-with-real-world-challenges-43d9c4dc91f7
- author_url
- https://medium.com/@imkss
- status
- ok
- fetched_at
- 2026-06-10 08:17:25