← Back to list

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…

Kumar sunny suman · 2026-02-27 09:56 · 0 claps · 6.9 min read
#angular #migration #frontend-development #primeng #angular-material
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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:

  1. Each version has specific migration schematics that automate common changes
  2. Breaking changes are easier to identify and fix in isolation
  3. 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, @switch syntax (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:

  1. pTemplate="header"#header
  2. pTemplate="body"#body
  3. pTemplate="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:

  1. Replacing deprecated Nebular with Angular Material and PrimeNG
  2. Adding standalone: false to all NgModule-based components
  3. Updating PrimeNG theme configuration to the new programmatic API
  4. Fixing PrimeNG table template syntax from pTemplate= to #name
  5. 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

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