Angular 129 🅰️ Upgrading Legacy Applications
Upgrading a legacy Angular application is rarely a single task. It is a sequence of decisions: which Angular version the application is on, whether the codebase still uses NgModules or standalone components, whether the build system is the old webpack-based one or the new esbuild-based one, and whether the application was originally written in AngularJS and migrated through the hybrid path. Each of these dimensions has its own migration tooling, and the order in which they are addressed determines whether the upgrade is a controlled change or a cascade of failures.
The Angular CLI provides ng update for version-to-version upgrades, and the Angular Update Guide at update.angular.io provides the version-specific checklist. For applications that have not yet adopted the modern build system, Angular 20 introduced a migration from the webpack-based browser builder to the esbuild-based application builder, which changes the build configuration and the SSR integration. For applications that started as AngularJS and were migrated through ngUpgrade, the legacy bridge may still be present and needs to be removed once the migration is complete.
This chapter covers the incremental version upgrade path, the build-system migration, the cleanup of legacy patterns, the upgrade of third-party dependencies, and the workflow that keeps a legacy application current without disrupting operations.
Key point: Upgrade one major version at a time using ng update @angular/cli@^N @angular/core@^N. Use the Angular Update Guide for version-specific instructions. Migrate the build system from the legacy browser builder to the application builder when ready. Remove ngUpgrade and AngularJS code once the hybrid migration is complete. Always work from a clean git state with passing tests.
Why legacy upgrades need a specific strategy
The version-skipping problem. Angular supports upgrading one major version at a time. Skipping versions is not supported because each version’s schematics assume the codebase is on the previous version. An application on Angular 15 must upgrade to 16, then 17, then 18, and so on .
The build-system problem. Angular 20 introduced the application builder as the default for new projects. Existing projects that use the browser builder must migrate to take advantage of the new build system. The migration changes the builder name and several build options, and it integrates SSR and prerendering into the @angular/ssr package .
The hybrid-cleanup problem. Applications that were migrated from AngularJS through ngUpgrade may still have the bridge in place. The UpgradeModule, downgradeComponent, and downgradeInjectable wrappers are temporary. Leaving them in place keeps AngularJS dependencies in the bundle and prevents the application from using modern Angular features fully .
The third-party problem. Libraries like Angular Material, NgRx, and ESLint plugins have their own release cycles. They may lag behind the Angular core, and updating them requires ng update support or manual version bumps. Peer dependency conflicts are common and must be resolved before the core upgrade can complete .
The environment problem. Each Angular major version requires a minimum Node.js and TypeScript version. Angular 18 requires Node.js 18.19 or later; Angular 20 requires Node.js 20 or later; Angular 22 requires TypeScript 6.0. The environment must be updated in step with the framework .
a. The incremental version upgrade
The upgrade path follows the same workflow for every major version. The commands are run one version at a time, and each step is verified before the next.
# Check the current version
ng version
# Check for available updates
ng update
# Upgrade core and CLI together
ng update @angular/cli@^16 @angular/core@^16
# Upgrade third-party packages
ng update @angular/material@^16 @angular/cdk@^16
# Verify the build and tests
ng build
npm test
The --force flag is used when peer dependency conflicts cannot be resolved. It should be a temporary measure, not a permanent part of the workflow .
The Angular Update Guide at update.angular.io provides the version-specific breaking changes. For Angular 16, TestBed.get() was removed in favor of TestBed.inject(). For Angular 17, the new control flow syntax is available, and the migration schematic ng generate @angular/core:control-flow converts *ngIf and *ngFor to @if and @for .
The upgrade from one version to the next is a distinct, verifiable step. The build must pass, the tests must pass, and the application must run before proceeding to the next version.
b. Migrating the build system
Angular 20 introduced the application builder, which replaces the browser builder for applications that use the new build system. The migration is a change in angular.json and several option renames.
The legacy configuration:
{
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"buildOptimizer": true,
"vendorChunk": true,
"commonChunk": true
}
}
}
The migrated configuration:
{
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"browser": "src/main.ts",
"polyfills": ["src/polyfills.ts"]
}
}
}
The main option is renamed to browser, the polyfills option becomes an array, and the buildOptimizer, vendorChunk, and commonChunk options are removed because they are no longer needed .
For applications that use SSR, the migration also removes the @nguniversal packages and replaces them with the @angular/ssr package. The app-shell, prerender, server, and ssr-dev-server builders are integrated into the application builder .
The migration is run with ng update when upgrading to Angular 20. The prompt for use-application-builder appears during the update, and accepting it performs the migration .
c. Cleaning up legacy patterns
A legacy application accumulates patterns that are no longer recommended. The upgrade is an opportunity to remove them.
The hybrid bridge. If the application was migrated from AngularJS, the ngUpgrade bridge should be removed once the migration is complete. The UpgradeModule, downgradeComponent, and downgradeInjectable wrappers are temporary scaffolding. Removing them eliminates the AngularJS dependency from the bundle and allows the application to use standalone components and modern change detection .
The control flow syntax. Angular 17 introduced @if, @for, and @switch as replacements for the structural directives. The migration schematic converts existing templates, but the track expressions often need manual refinement. The schematic cannot infer the unique identifier, so it may use the object reference, which is slower. Reviewing and replacing the track expressions with a unique property like id improves rendering performance .
The deprecated APIs. Each major version deprecates APIs that are removed in a later version. The Angular Update Guide lists the deprecations for each version, and the upgrade schematics flag the usages that need manual attention. Resolving them during the upgrade avoids a larger migration later.
The dead code. A legacy application often contains code that is no longer used. A dead code audit before the upgrade reduces the scope and makes the migration faster. Tools like unimported or the Angular compiler’s diagnostics can identify unused code.
d. Upgrading third-party dependencies
After the core upgrade, the third-party dependencies must be updated. Libraries that support ng update provide their own schematics.
ng update rxjs
ng update @angular-eslint/schematics
ng update @angular/material
ng update @ngrx/store
Libraries that do not support ng update must be updated manually. The library’s changelog or migration guide describes the breaking changes and the required code adjustments. The package.json version is updated, and the code is adjusted to match the new API.
If a library lags behind the Angular version, the upgrade must wait. This is one reason the incremental path is recommended: it gives the ecosystem time to catch up between major versions .
Libraries that are deprecated or in maintenance mode should be replaced. Protractor was deprecated in favor of modern testing frameworks. moment.js is often replaced by dayjs or date-fns. tslint is replaced by ESLint. The upgrade is an opportunity to make these replacements.
e. The safe upgrade workflow
The workflow for a legacy upgrade follows a consistent pattern.
1. Establish a baseline. Run ng version, node -v, and npm test. The tests must pass before the upgrade begins .
2. Commit a clean state. The upgrade modifies many files. A clean git state allows a revert if something goes wrong .
3. Check the Update Guide. Visit update.angular.io and select the current and target versions. The guide lists the breaking changes and the required steps.
4. Upgrade one version at a time. Run ng update @angular/cli@^N @angular/core@^N for each version. Verify the build and tests after each step .
5. Update third-party dependencies. Run ng update for each library that supports it. Update the others manually.
6. Migrate the build system. When upgrading to Angular 20 or later, accept the use-application-builder prompt or migrate manually.
7. Clean up legacy patterns. Remove the ngUpgrade bridge if present. Review the control flow track expressions. Resolve deprecated API usages.
8. Verify the build, tests, and application. Run ng build, npm test, and the development server. Exercise the critical flows.
9. Commit the upgrade. Commit the upgrade as a single change or a series of changes, and document the manual adjustments.
Complete Example Session
# ============================================
# PART 1: CHECK CURRENT STATE
# ============================================
ng version
node -v
npm test
# ============================================
# PART 2: COMMIT CLEAN STATE
# ============================================
git status
git add .
git commit -m "chore: prepare for angular upgrade"
# ============================================
# PART 3: UPGRADE TO ANGULAR 16
# ============================================
ng update @angular/cli@^16 @angular/core@^16
ng update @angular/material@^16 @angular/cdk@^16
ng build && npm test
# ============================================
# PART 4: UPGRADE TO ANGULAR 17
# ============================================
ng update @angular/cli@^17 @angular/core@^17
ng generate @angular/core:control-flow
ng build && npm test
# ============================================
# PART 5: UPGRADE TO ANGULAR 18
# ============================================
ng update @angular/cli@^18 @angular/core@^18
ng build && npm test
# ============================================
# PART 6: UPGRADE TO ANGULAR 20
# ============================================
ng update @angular/cli@^20 @angular/core@^20
# Accept the use-application-builder prompt
ng build && npm test
# ============================================
# PART 7: MIGRATE BUILD SYSTEM
# ============================================
# The ng update to Angular 20 runs the migration
# angular.json: browser → application
# main → browser, polyfills → array
# ============================================
# PART 8: CLEAN UP LEGACY PATTERNS
# ============================================
# Remove ngUpgrade if present
# Review control flow track expressions
# Resolve deprecated API usages
# ============================================
# PART 9: UPDATE THIRD-PARTY DEPENDENCIES
# ============================================
ng update rxjs
ng update @angular-eslint/schematics
ng update @ngrx/store
# ============================================
# PART 10: VERIFY AND COMMIT
# ============================================
ng build --configuration production
npm test
git add . && git commit -m "chore: upgrade to Angular 20"
These ten parts cover checking the current state, committing a clean state, upgrading through versions 16, 17, 18, and 20, migrating the build system, cleaning up legacy patterns, updating third-party dependencies, and verifying and committing.
Quick Reference
Upgrade Commands
| Command | Purpose |
|---|---|
ng version | Check current version |
ng update | Check for available updates |
ng update @angular/cli@^N @angular/core@^N | Upgrade to major version N |
ng update rxjs | Update a third-party package |
ng generate @angular/core:control-flow | Migrate control flow syntax |
Version Requirements
| Angular | Node.js | TypeScript |
|---|---|---|
| 16 | 16.14+ | 4.9+ |
| 17 | 18.13+ | 5.2+ |
| 18 | 18.19+ | 5.4+ |
| 20 | 20.19+ | 5.8+ |
| 22 | 20+ | 6.0+ |
Build System Migration
| Legacy | New |
|---|---|
browser builder | application builder |
main option | browser option |
polyfills string | polyfills array |
buildOptimizer | Removed |
vendorChunk | Removed |
commonChunk | Removed |
@nguniversal/* | @angular/ssr |
Legacy Cleanup
| Pattern | Action |
|---|---|
ngUpgrade bridge | Remove after migration |
*ngIf / *ngFor | Migrate to @if / @for |
track item | Replace with track item.id |
| Deprecated APIs | Resolve per Update Guide |
| Dead code | Audit and remove |
Upgrade Workflow
| Step | Action |
|---|---|
| 1 | Check versions and tests |
| 2 | Commit clean state |
| 3 | Check Update Guide |
| 4 | Upgrade one version at a time |
| 5 | Update third-party dependencies |
| 6 | Migrate build system |
| 7 | Clean up legacy patterns |
| 8 | Verify build, tests, application |
| 9 | Commit |
Best Practices
✅ Do This:
# Upgrade one version at a time
ng update @angular/cli@^16 @angular/core@^16
# Verify after each step
ng build && npm test
# Migrate control flow syntax
ng generate @angular/core:control-flow
# Remove ngUpgrade after migration
# Delete the bridge and AngularJS dependencies
# Use the Update Guide
# Visit update.angular.io for version-specific instructions
❌ Don’t Do This:
# Skip versions
ng update @angular/core@^20 # ❌ from 18 to 20 is not supported
# Upgrade with uncommitted changes
ng update @angular/core # ❌ no safety net
# Ignore peer dependency conflicts
ng update --force # ❌ resolve the conflicts first
# Leave the build system on the legacy builder
# Migrate to the application builder when ready # ❌
# Leave the ngUpgrade bridge in place
# Remove it after the migration is complete # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Peer dependency conflict | Third-party library not updated | Update the library or use --force temporarily |
| Build fails after upgrade | Breaking change not handled | Check the Update Guide for the version |
| Tests fail | Test utility API changed | Update the test code |
| Node.js version too old | Environment not updated | Update Node.js to the required version |
| Build system not migrated | Prompt skipped | Accept the use-application-builder prompt or migrate manually |
| ngUpgrade still in bundle | Bridge not removed | Remove the bridge and AngularJS code |
| Suboptimal track expression | Schematic cannot infer unique ID | Review and replace with item.id |
Real-World Examples
1. Check Version
ng version
2. Upgrade Core and CLI
ng update @angular/cli@^20 @angular/core@^20
3. Update Third-Party
ng update @angular/material@^20 @angular/cdk@^20
4. Migrate Control Flow
ng generate @angular/core:control-flow
5. Migrate Build System
{
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"browser": "src/main.ts",
"polyfills": ["src/polyfills.ts"]
}
}
}
6. Remove ngUpgrade
// Remove UpgradeModule, downgradeComponent, downgradeInjectable
// Remove AngularJS module and dependencies
7. Fix Track Expression
<!-- Before: track item -->
@for (item of items; track item.id) { }
8. Verify Build
ng build --configuration production
9. Run Tests
npm test
10. Commit Upgrade
git add . && git commit -m "chore: upgrade to Angular 20"
Visual
Incremental Upgrade Path
┌──────────────────────────────────────────────────────────────┐
│ Angular 15 │
│ │ │
│ ▼ ng update @angular/cli@^16 @angular/core@^16 │
│ Angular 16 │
│ │ │
│ ▼ ng update @angular/cli@^17 @angular/core@^17 │
│ Angular 17 │
│ │ ng generate @angular/core:control-flow │
│ ▼ │
│ Angular 18 │
│ │ │
│ ▼ ng update @angular/cli@^20 @angular/core@^20 │
│ Angular 20 │
│ │ Accept use-application-builder prompt │
│ ▼ │
│ Angular 22 │
│ │
│ One major version at a time. Verify each step. │
└──────────────────────────────────────────────────────────────┘
Build System Migration
┌──────────────────────────────────────────────────────────────┐
│ LEGACY (browser builder): │
│ "builder": "@angular-devkit/build-angular:browser" │
│ "main": "src/main.ts" │
│ "polyfills": "src/polyfills.ts" │
│ "buildOptimizer": true │
│ "vendorChunk": true │
│ │
│ MODERN (application builder): │
│ "builder": "@angular-devkit/build-angular:application" │
│ "browser": "src/main.ts" │
│ "polyfills": ["src/polyfills.ts"] │
│ (buildOptimizer, vendorChunk, commonChunk removed) │
└──────────────────────────────────────────────────────────────┘
Legacy Cleanup
┌──────────────────────────────────────────────────────────────┐
│ BEFORE: │
│ ├── ngUpgrade bridge (UpgradeModule) │
│ ├── *ngIf / *ngFor in templates │
│ ├── track item (object reference) │
│ ├── Deprecated APIs │
│ └── Dead code │
│ │
│ AFTER: │
│ ├── Bridge removed │
│ ├── @if / @for in templates │
│ ├── track item.id │
│ ├── Deprecated APIs resolved │
│ └── Dead code removed │
└──────────────────────────────────────────────────────────────┘
Upgrade Workflow
┌──────────────────────────────────────────────────────────────┐
│ 1. Check versions and tests │
│ │ │
│ ▼ │
│ 2. Commit clean state │
│ │ │
│ ▼ │
│ 3. Check Update Guide │
│ │ │
│ ▼ │
│ 4. Upgrade one version at a time │
│ │ │
│ ▼ │
│ 5. Update third-party dependencies │
│ │ │
│ ▼ │
│ 6. Migrate build system │
│ │ │
│ ▼ │
│ 7. Clean up legacy patterns │
│ │ │
│ ▼ │
│ 8. Verify and commit │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Upgrade command | ng update @angular/cli@^N @angular/core@^N |
| Version path | One major version at a time |
| Update Guide | update.angular.io |
| Build system migration | browser → application builder |
| Control flow migration | ng generate @angular/core:control-flow |
| ngUpgrade cleanup | Remove after migration |
| Track expression | Review and replace with unique ID |
| Node.js requirement | Varies by version |
| TypeScript requirement | Varies by version |
| Safety net | Clean git state, passing tests |
Key takeaways:
- Upgrade one major version at a time. Angular does not support skipping versions because each version’s schematics assume the codebase is on the previous version. The
ng updatecommand handles the mechanical changes . - The build system migrates from
browsertoapplication. Angular 20 introduced the esbuild-basedapplicationbuilder. The migration renamesmaintobrowser, makespolyfillsan array, and removes the legacy optimization options . - The control flow syntax migration is a separate step. Angular 17 introduced
@if,@for, and@switch. The migration schematic converts existing templates, but thetrackexpressions need manual refinement . - The
ngUpgradebridge is temporary. Applications migrated from AngularJS throughngUpgradeshould remove the bridge once the migration is complete. Leaving it in place keeps AngularJS dependencies in the bundle . - Third-party dependencies must be updated after the core. Libraries that support
ng updateprovide their own schematics. Libraries that do not must be updated manually, and libraries that lag behind the Angular version cause the upgrade to wait . - The safe workflow is consistent. Commit a clean state, verify tests pass, run
ng updateone version at a time, resolve flagged issues, update third-party dependencies, migrate the build system, clean up legacy patterns, and verify the build and tests . - The Angular Update Guide is the version-specific reference. It lists the breaking changes, the deprecations, and the manual steps for each version transition.
Remember: Upgrading a legacy Angular application is not a single command. It is a sequence of version upgrades, each with its own schematics and manual steps. The build system migration, the control flow migration, the ngUpgrade cleanup, and the third-party dependency updates are separate tasks that follow the version upgrades. The safe workflow is always the same: check, commit, verify, upgrade, resolve, verify, commit. With a clean git state and a passing test suite, the upgrade is a controlled change rather than a risky leap. The Angular team designed the update tooling to make version migration predictable, and following the prescribed path keeps applications current without disrupting operations.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!