| |

Angular 129 🅰️ Upgrading Legacy Applications

⏱️ Estimated read: 13 min | |

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

CommandPurpose
ng versionCheck current version
ng updateCheck for available updates
ng update @angular/cli@^N @angular/core@^NUpgrade to major version N
ng update rxjsUpdate a third-party package
ng generate @angular/core:control-flowMigrate control flow syntax

Version Requirements

AngularNode.jsTypeScript
1616.14+4.9+
1718.13+5.2+
1818.19+5.4+
2020.19+5.8+
2220+6.0+

Build System Migration

LegacyNew
browser builderapplication builder
main optionbrowser option
polyfills stringpolyfills array
buildOptimizerRemoved
vendorChunkRemoved
commonChunkRemoved
@nguniversal/*@angular/ssr

Legacy Cleanup

PatternAction
ngUpgrade bridgeRemove after migration
*ngIf / *ngForMigrate to @if / @for
track itemReplace with track item.id
Deprecated APIsResolve per Update Guide
Dead codeAudit and remove

Upgrade Workflow

StepAction
1Check versions and tests
2Commit clean state
3Check Update Guide
4Upgrade one version at a time
5Update third-party dependencies
6Migrate build system
7Clean up legacy patterns
8Verify build, tests, application
9Commit

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

PitfallWhy It HappensFix
Peer dependency conflictThird-party library not updatedUpdate the library or use --force temporarily
Build fails after upgradeBreaking change not handledCheck the Update Guide for the version
Tests failTest utility API changedUpdate the test code
Node.js version too oldEnvironment not updatedUpdate Node.js to the required version
Build system not migratedPrompt skippedAccept the use-application-builder prompt or migrate manually
ngUpgrade still in bundleBridge not removedRemove the bridge and AngularJS code
Suboptimal track expressionSchematic cannot infer unique IDReview 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

ItemValue
Upgrade commandng update @angular/cli@^N @angular/core@^N
Version pathOne major version at a time
Update Guideupdate.angular.io
Build system migrationbrowser → application builder
Control flow migrationng generate @angular/core:control-flow
ngUpgrade cleanupRemove after migration
Track expressionReview and replace with unique ID
Node.js requirementVaries by version
TypeScript requirementVaries by version
Safety netClean 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 update command handles the mechanical changes .
  • The build system migrates from browser to application. Angular 20 introduced the esbuild-based application builder. The migration renames main to browser, makes polyfills an 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 the track expressions need manual refinement .
  • The ngUpgrade bridge is temporary. Applications migrated from AngularJS through ngUpgrade should 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 update provide 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 update one 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.


Was this helpful?


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!