Wanna see something cool? Check out Angular Spotify 🎧

Upgrade to Angular 20 from Angular 13 - Part 4: Angular 17 with Claude Code

We are back. In Part 3, we upgraded Jira Clone from Angular 15 to 16 using Claude Code, and the whole thing was mostly just dependency bumps. I mentioned at the end that Angular 17 would be the interesting one. It was.

Angular 17 is not just another version bump. This is the release where standalone components become the default, the new control flow syntax (@if, @for, @switch) replaces structural directives, and there is an entirely new build system available. For the first time in this series, we need actual code changes, not just package.json updates.

The approach

Same as Part 3. I gave Claude the previous blog posts and the implementation plan from the Angular 16 upgrade, and asked it to generate a new plan for Angular 17.

Convert to standalone components

The difference this time is that the plan had two phases:

  1. Phase 1: Dependency upgrades - the same proven pattern from Parts 1-3
  2. Phase 2: Code migration - standalone components, standalone bootstrap, and new control flow syntax

Claude produced a 19-task plan covering everything from the initial ng update all the way to creating the PR. The full plan is available in the repo.

Phase 1: Dependency upgrades

This part was familiar. Same pattern as before:

  1. Run ng update @angular/core@17 @angular/cli@17 --force
  2. Resolve each dependency conflict one at a time
  3. Commit after each step

One nice thing: ng update automatically bumped TypeScript to 5.4.5 and zone.js to 0.14.10 this time, so two tasks from the plan were already done.

The remaining dependencies followed the usual flow:

Package Before (v16) After (v17)
@angular/core ^16.2.12 ^17.3.12
@angular/cli ^16.2.16 ^17.3.17
typescript ~5.1.6 ~5.4.5
zone.js ~0.13.3 ~0.14.10
ng-zorro-antd ^16.2.2 ^17.4.1
ngx-quill ^22.1.1 ^24.0.5
@angular/cdk ^16.2.14 ^17.3.10
@angular-eslint/* 16.3.1 17.5.3
@typescript-eslint/* 5.11.0 6.21.0

For ngx-quill, it was important to stay on v24.x and not jump to v25+. Version 25 requires Quill v2, which is a completely different API. Version 24 supports Angular 17 while keeping Quill v1. That saved us from a much bigger migration.

After resolving all dependencies, npm install ran cleanly without --force. Build passed on the first try.

Phase 2: The real work

This is where it got interesting. Angular 17 provides CLI schematics to automate the three major code migrations:

Step 1: Convert to standalone components

npx ng generate @angular/core:standalone --mode convert-to-standalone

This schematic went through all 42 components and 1 directive, added standalone: true to each one, and moved the necessary imports from their parent NgModules into each component’s own imports array. One command, 46 files updated.

Convert to standalone components

Step 2: Prune NgModules

npx ng generate @angular/core:standalone --mode prune-ng-modules

This removed NgModule classes that were no longer needed. In our case, only SnowModule was fully pruned. The other modules like ProjectModule and WorkInProgressModule were kept because they still serve as entry points for lazy-loaded routes.

There was one issue here. The schematic deleted SnowModule but forgot to add SnowComponent to the imports of AppModule. The build failed with 'j-snow' is not a known element. Quick fix, added the import manually.

Prune NgModules

Step 3: Migrate to standalone bootstrap

npx ng generate @angular/core:standalone --mode standalone-bootstrap

This was the big one. It converted main.ts from:

platformBrowserDynamic().bootstrapModule(AppModule)

to:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(BrowserModule, AppRoutingModule, ...),
    provideAnimations(),
    provideHttpClient(withInterceptorsFromDi()),
    // Sentry, Akita, and other providers
  ]
})

And then deleted AppModule entirely. The schematic handled most of it correctly, but missed the import * as Sentry statement that was previously in app.module.ts. Another quick manual fix.

Migrate to standalone bootstrap

Step 4: Migrate control flow syntax

npx ng generate @angular/core:control-flow

This converted all *ngIf and *ngFor directives to the new @if and @for syntax across 26 template files. For example:

<!-- Before -->
<div *ngIf="isLoading; else content">Loading...</div>

<!-- After -->
@if (isLoading) {
  <div>Loading...</div>
} @else {
  ...
}

The @for syntax requires a track expression, which the schematic handled automatically. No track $index fallbacks needed.

One thing the new control flow exposed: stricter type checking. Two interface classes (IssuePriorityIcon and IssueTypeWithIcon) had their value field typed as string when the methods consuming them expected enum types (IssuePriority and IssueType). The old *ngFor was lenient about this. The new @for was not. Fixed by updating the type definitions to use the correct enum types.

ESLint cleanup

The @angular-eslint v17 removed the ng-cli-compat config that the project was using. Replaced it with @angular-eslint/recommended and @typescript-eslint/recommended. Also had to upgrade @typescript-eslint/* from 5.11.0 to 6.21.0 because the old version did not support TypeScript 5.4.

ESLint cleanup

The result

Angular 17 migration result

13 minutes, 13 commits. Application compiles and serves without errors.

The final commit logs:

027b803 fix: update eslint config and typescript-eslint for Angular 17 compatibility
10a8a74 refactor: migrate to new control flow syntax (@if, @for)
d31575e refactor: migrate to standalone bootstrap with bootstrapApplication()
64080a6 refactor: prune NgModules after standalone conversion
e150df6 refactor: convert all components to standalone
9d3bc65 build(deps): clean npm install with resolved Angular 17 dependencies
c5747ba build(deps): upgrade ngx-quill to 24.x for Angular 17
712f44e build(deps): upgrade ng-zorro-antd to 17.4.1
8707083 build(deps): upgrade @angular/cdk to 17.3.10
559a5d6 build(deps): upgrade @angular-eslint/* to 17.5.3
0f31878 build(deps): upgrade @angular-builders/custom-webpack to 17.0.2
418ec24 build(deps): ng update @angular/core@17 @angular/cli@17 --force

Source code

https://github.com/trungvose/jira-clone-angular/pull/110

What I learned

The Angular CLI schematics for standalone migration and control flow are surprisingly good. They handled 90% of the work automatically. The remaining 10% were edge cases: a missing import here, a stricter type check there. Nothing that took more than a minute to fix.

The two-phase approach worked well. Phase 1 (dependency upgrades) followed the exact same pattern from the previous three parts. Phase 2 (code migration) was new territory, but the schematics did the heavy lifting.

What surprised me was how much code changed. 54 files were touched by the control flow migration alone. 46 files by the standalone conversion. If I had done this manually, it would have been a full day of tedious find-and-replace work. The schematics turned it into a few commands.

One thing worth noting: the Storybook migration (v6 to v7) was intentionally skipped. Storybook 6.x still works with Angular 17, just with some peer dependency warnings. The v6 to v7 upgrade is a separate, non-trivial migration that deserves its own PR.

What is next

We are at Angular 17. The remaining path is 17 -> 18 -> 19 -> 20. Angular 18 should be relatively smooth since the major architectural changes (standalone, control flow) are already done. The esbuild migration is something we will need to tackle eventually, but that can wait.

See you in Part 5.

Published 12 Mar 2026

Read more

 — Upgrade to Angular 20 from Angular 13 - Part 3: Angular 16 with Claude Code
 — I added synced lyrics to Angular Spotify without writing a single line of code
 — Google TypeScript Style Guide
 — Typescript types vs interface
 — Upgrade to Angular 20 from Angular 13 - Part 2: Angular 15