Refactoring a Legacy Codebase with Claude Code
How I used Claude Code and the grill-me skill to finally plan a proper cleanup of my website codebase.
Stable signals, better templates, and performance by default
Nicky Haze - 13 min read
Angular keeps moving fast and with Angular 22, released in June 2026, the framework takes another important step towards becoming more reactive, performant and developer-friendly.
Angular 21 introduced several promising APIs, such as Signal Forms and Angular Aria. Angular 22 now makes many of those APIs production-ready, while also improving templates, dependency injection, routing and change detection.
The most important Angular 22 features are:
resource() and httpResource() are now stableOnPush is the default for new applications@Service decoratorinjectAsync()@switch blocksAngular 22 may not completely change how we write Angular applications overnight, but it makes many of Angular's modern APIs ready for real production applications.
Angular 22 also marks an important change in how Angular itself will be released.
Until Angular 22, the Angular team published a new major version approximately every six months. Starting with Angular 22, Angular is moving to a new release cycle:
Angular 23 is therefore expected around June 2027, rather than at the end of 2026.
This does not mean Angular development is slowing down. More features can arrive through backward-compatible minor releases instead of being held back for the next major version.
For development teams, this should make upgrades easier to plan. Breaking changes and required migrations can still be introduced in major releases, but that work now happens once per year instead of every six months.
Angular's support window remains 24 months:
I think this is a positive change, especially for larger enterprise applications. Angular can continue evolving quickly, while teams get a longer and more predictable period between major migrations.
Signal Forms were introduced in Angular 21 as an experimental API. In Angular 22, they have officially become stable and production-ready.
This is a major milestone because forms have traditionally been one of the more complex parts of Angular. Reactive Forms provide plenty of structure, but this often comes with boilerplate, nested FormGroup objects and RxJS-based state management.
Signal Forms provide a more declarative approach where the form model, validation state and field values are represented using signals.
import { Component, signal } from '@angular/core'; import { form, FormField, required, } from '@angular/forms/signals'; @Component({ selector: 'app-payment-form', imports: [FormField], template: ` <form (submit)="onSubmit($event)"> <label for="payment-type">Payment type</label> <select id="payment-type" [formField]="paymentForm.paymentType" > <option value="">Select a payment method</option> <option value="credit-card">Credit card</option> <option value="paypal">PayPal</option> </select> @if ( paymentForm.paymentType().invalid() && paymentForm.paymentType().touched() ) { <p class="error"> Please select a payment method. </p> } <label for="amount">Amount</label> <input id="amount" type="number" [formField]="paymentForm.amount" /> <button type="submit" [disabled]="paymentForm().invalid()" > Pay </button> </form> `, }) export class PaymentFormComponent { readonly payment = signal({ paymentType: '', amount: 0, }); readonly paymentForm = form(this.payment, (payment) => { required(payment.paymentType, { message: 'Please select a payment method', }); required(payment.amount, { message: 'Please enter an amount', }); }); onSubmit(event: SubmitEvent): void { event.preventDefault(); if (this.paymentForm().invalid()) { return; } console.log('Payment submitted', this.payment()); } }
The form is directly connected to the payment signal. Updating an input updates the model and changing the model updates the corresponding field. Form state, validation state and UI reactivity are all represented through the same primitive and become part of Angular's signal ecosystem. This reduces the mental overhead of switching between signals, RxJS streams and Reactive Forms APIs.
There is no separate FormGroup, no valueChanges subscription and no need to manually synchronise the form with another piece of state.
Love it!
Angular 22 also adds official Signal Forms support for Angular Material and Angular Aria.
This is important for adoption. A forms API only becomes truly useful when it integrates properly with the component libraries we use in real applications.
Signal Forms are no longer just an interesting experiment. They are now a realistic option for production applications.
Application state is not always synchronous. Most applications eventually need to load data from an API, database or another asynchronous source.
Angular's resource() and httpResource() APIs bridge that gap. Both APIs are now stable in Angular 22.
The resource() API connects asynchronous data to signals without requiring us to manually manage loading, error and success states.
import { Component, resource, signal, } from '@angular/core'; interface WeatherForecast { temperature: number; condition: string; } @Component({ selector: 'app-weather', template: ` <select [value]="selectedCity()" (change)="selectedCity.set( $any($event.target).value )" > <option value="Amsterdam">Amsterdam</option> <option value="Rotterdam">Rotterdam</option> <option value="Utrecht">Utrecht</option> </select> @if (weather.isLoading()) { <p>Loading weather...</p> } @else if (weather.error()) { <p>Something went wrong.</p> } @else if (weather.hasValue()) { <h2>{{ selectedCity() }}</h2> <p>{{ weather.value().temperature }}°C</p> <p>{{ weather.value().condition }}</p> } `, }) export class WeatherComponent { readonly selectedCity = signal('Amsterdam'); readonly weather = resource({ params: () => ({ city: this.selectedCity(), }), loader: ({ params, abortSignal }) => fetch( `/api/weather/${encodeURIComponent(params.city)}`, { signal: abortSignal }, ).then((response) => response.json() as Promise<WeatherForecast>), }); }
When selectedCity changes, the resource automatically reloads its data.
It also provides reactive state for loading, errors, resolved values, reloading and request cancellation. This removes a lot of repetitive state management from our components.
For regular HTTP calls, Angular provides httpResource().
import { Component, signal } from '@angular/core'; import { httpResource } from '@angular/common/http'; interface User { id: string; name: string; email: string; } @Component({ selector: 'app-user-details', template: ` @if (user.isLoading()) { <p>Loading user...</p> } @else if (user.error()) { <p>Could not load the user.</p> } @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> <p>{{ user.value().email }}</p> } `, }) export class UserDetailsComponent { readonly userId = signal('user-1'); readonly user = httpResource<User>( () => `/api/users/${this.userId()}`, ); }
The URL is reactive. Whenever userId changes, Angular performs a new request and updates the resource state.
Resources are primarily intended for read operations, which usually means GET requests. Angular's documentation recommends using HttpClient directly for mutations such as POST or PUT.
For advanced workflows, RxJS and HttpClient remain extremely useful. But for straightforward reactive data loading, httpResource() provides a clean and understandable alternative.
Angular Aria was introduced as a developer preview in Angular 21. In Angular 22, it is now stable.
Angular Aria provides unstyled, accessible UI primitives. The directives implement complex accessibility behaviour, while you remain responsible for styling and visual design.
This is especially useful when creating your own design system. You do not have to rebuild keyboard navigation, focus management and ARIA behaviour from scratch, but you are also not forced to use Angular Material's visual appearance.
import { Component } from '@angular/core'; import { Tabs, TabList, Tab, TabPanel, } from '@angular/aria/tabs'; @Component({ selector: 'app-account-tabs', imports: [ Tabs, TabList, Tab, TabPanel, ], template: ` <div ngTabs> <div ngTabList aria-label="Account settings"> <button ngTab value="profile">Profile</button> <button ngTab value="security">Security</button> </div> <div ngTabPanel value="profile"> Profile settings </div> <div ngTabPanel value="security"> Security settings </div> </div> `, }) export class AccountTabsComponent {}
Angular Aria handles the underlying accessibility pattern, including keyboard interaction and focus behaviour.
Angular 22 also adds Signal Forms integration, test harnesses and twelve supported UI patterns. This makes Angular Aria a strong foundation for organisations that want accessible, completely custom-styled components.
One of the most important changes in Angular 22 is that OnPush change detection is now the default for new applications.
Before Angular 22, we commonly wrote:
@Component({ selector: 'app-user-card', templateUrl: './user-card.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserCardComponent {}
In new Angular 22 applications, the following component already uses OnPush behaviour:
@Component({ selector: 'app-user-card', templateUrl: './user-card.html', }) export class UserCardComponent {}
No additional configuration is required.
This fits perfectly with Angular's direction:
OnPush prevents unnecessary component checksThe old ChangeDetectionStrategy.Default name has also been renamed to ChangeDetectionStrategy.Eager.
@Component({ selector: 'app-legacy-widget', templateUrl: './legacy-widget.html', changeDetection: ChangeDetectionStrategy.Eager, }) export class LegacyWidgetComponent {}
Eager describes the behaviour more clearly. Existing applications are not migrated automatically, so upgrading should not unexpectedly change the strategy of existing components.
Angular 22 introduces a new @Service() decorator.
Until now, a globally provided service was generally written like this:
@Injectable({ providedIn: 'root', }) export class UserService {}
Angular 22 allows us to express the same intention more clearly:
import { Service } from '@angular/core'; @Service() export class UserService {}
By default, @Service() behaves like @Injectable({ providedIn: 'root' }).
That means the service:
import { inject, Service, } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Service() export class UserService { private readonly http = inject(HttpClient); getCurrentUser() { return this.http.get('/api/users/me'); } }
Not every service should be application-wide. Automatic root provisioning can therefore be disabled:
@Service({ autoProvided: false, }) export class CheckoutState {}
The service can then be provided at component or route level:
@Component({ selector: 'app-checkout', providers: [CheckoutState], template: `...`, }) export class CheckoutComponent { readonly checkoutState = inject(CheckoutState); }
The distinction is straightforward:
@Service() for a root-provided application service@Service({ autoProvided: false }) for explicitly scoped services@Injectable() when you need constructor injection or advanced provider configuration@Injectable() is not disappearing. But for regular root-provided services, @Service() communicates the intention more clearly and removes some boilerplate.
Angular has supported lazy-loaded routes and components for a long time, but services were harder to load on demand.
Angular 22 introduces injectAsync(), allowing Angular to load a service asynchronously when it is first needed.
// report-exporter.ts import { Service } from '@angular/core'; @Service() export class ReportExporter { export(): void { console.log('Exporting report...'); } }
import { Component, injectAsync, } from '@angular/core'; @Component({ selector: 'app-report', template: ` <button type="button" (click)="exportReport()"> Export report </button> `, }) export class ReportComponent { private readonly reportExporter = injectAsync( () => import('./report-exporter').then( ({ ReportExporter }) => ReportExporter, ), ); async exportReport(): Promise<void> { const exporter = await this.reportExporter(); exporter.export(); } }
The export service and its dependencies are only loaded when the user clicks the button for the first time.
This can be useful for larger features such as PDF generation, spreadsheet exports, charting libraries, complex editors and administration tools.
Angular can also prefetch an asynchronous dependency when the browser becomes idle:
private readonly reportExporter = injectAsync( () => import('./report-exporter').then( ({ ReportExporter }) => ReportExporter, ), { prefetch: onIdle, }, );
This gives applications more control over when code is downloaded without adding it to the initial bundle.
Angular 22 introduces several template improvements. They may look small individually, but together they make templates more expressive and reduce unnecessary component code.
Angular templates now support spread syntax for objects, arrays and function arguments.
<div [class]="{ ...baseClasses(), selected: isSelected(), disabled: isDisabled() }" > Account </div>
<app-order-summary [items]="[ ...standardItems(), ...additionalItems() ]" />
Previously, we would often create an additional computed signal or component method just to combine these values.
I would still keep complex transformations outside the template. But for simple cases, spread syntax can make templates much cleaner.
Angular 22 also allows arrow functions inside templates.
<button type="button" (click)="product.update( current => ({ ...current, stock: current.stock - 1 }) )" > Decrease stock </button>
Before Angular 22, this usually required a separate component method.
I would not start writing large functions inside templates. Templates should remain readable and focused on presentation.
For small signal updates and simple callbacks, however, arrow functions are a welcome addition.
Angular 22 supports comments between an element's attributes and bindings.
<app-user-card /* Used inside the sidebar */ [compact]="true" // Do not display archived users [showArchived]="false" [user]="selectedUser()" />
This can be useful for complex components with many inputs, especially when a binding requires additional context.
Multiple cases can now share a single block:
@switch (orderStatus()) { @case ('pending') @case ('processing') { <span>In progress</span> } @case ('shipped') { <span>Shipped</span> } @case ('cancelled') { <span>Cancelled</span> } }
Angular can also verify that every possible union value is handled:
@switch (orderStatus()) { @case ('pending') @case ('processing') { <span>In progress</span> } @case ('shipped') { <span>Shipped</span> } @case ('cancelled') { <span>Cancelled</span> } @default never; }
Using @default never; tells Angular that the switch should be exhaustive.
When another status is added later, Angular can report that the template has not handled it. This brings TypeScript-like exhaustive checking into Angular templates and is a great improvement for maintainability.
Angular 22 introduces experimental integration between the Angular Router and the browser's native Navigation API.
provideRouter( routes, withExperimentalPlatformNavigation(), );
This allows Angular to work more closely with the browser's native navigation lifecycle.
Potential benefits include:
RouterLink and regular anchor navigationsThe API is still experimental, so I would not enable it blindly in every production application. It is, however, an interesting indication of Angular aligning more closely with modern browser APIs.
Angular 22 also introduces experimental automatic cleanup for route-level injectors:
provideRouter( routes, withExperimentalAutoCleanupInjectors(), );
When enabled, Angular destroys dependency injectors belonging to routes that are no longer active. This can prevent unused route-level services and resources from remaining in memory.
For applications using a custom RouteReuseStrategy, the new destroyDetachedRouteHandle() API provides an official way to destroy cached route components.
These improvements are especially interesting for large enterprise applications where users move through many feature areas during long-running sessions.
Angular 22 officially supports TypeScript 6.
Angular 22.0 requires:
22.22.3, 24.15.0, 26.0.0 or a compatible newer patch release6.06.5.3 or 7.4.0 and later compatible releasesThis is important to consider before upgrading. Especially in enterprise environments, the required Node.js version can affect local development environments, CI runners and internal build images.
Always check Angular's official compatibility table before starting the migration.
Angular 22 continues investing in AI-assisted development.
The Angular CLI MCP server now provides stable tools that allow coding agents to start development servers, inspect builds, run tests, execute end-to-end tests and assist with Angular migrations.
Angular 22 also introduces official Angular Agent Skills. These give AI agents up-to-date instructions and examples for modern Angular development, including Signal Forms and Angular Aria.
This is becoming increasingly important. AI models often still suggest outdated Angular patterns, such as unnecessary NgModules, constructor injection and older template syntax.
Official Angular-specific context should help coding agents generate code that better matches the current framework.
Angular 22 introduces experimental support for WebMCP.
WebMCP allows web applications to expose structured tools that an AI agent can call from within the browser.
Instead of manipulating the application purely through DOM selectors, an application could expose actions such as:
Angular's early integration supports tools defined at application, route and service level. It can also generate tools from Signal Forms.
This is still experimental and browser support is developing, but it provides an interesting glimpse into how web applications may interact with AI agents in the future.
Angular 22 deprecates several parts of Angular's older Webpack-based build infrastructure, including:
@angular-devkit/build-angular builders@ngtools/webpackAngular has been moving towards its modern application builder, using tools such as esbuild and Vite, for multiple releases.
Existing Webpack applications will not immediately stop working. Angular provides an automated migration to help applications using the standard browser builder move to the modern application builder:
ng update @angular/cli --name use-application-builder
The migration updates angular.json, application code and stylesheets where possible. Teams using custom Webpack builders should also check the documentation of their builder, because custom configuration and plugins may still require manual changes. Angular's build-system migration guide documents the automated and manual migration paths, common compatibility issues and the browser-esbuild compatibility builder.
For regular Angular CLI applications that already use the application builder, the impact should be limited.
The Angular team also previewed @boundary, a template-level error boundary API.
@boundary { <app-recommendations /> } @error { <app-recommendations-fallback /> } <app-shopping-cart /> <app-checkout />
When the recommendations component throws an error, Angular can display fallback content without breaking the shopping cart and checkout flow around it.
This could become useful for dashboards, e-commerce applications and applications containing independent widgets.
However, @boundary is not part of the initial Angular 22 release. It was announced as a developer preview planned for the third quarter of 2026.
It is therefore something to keep an eye on rather than something to use immediately.
You can update an Angular CLI application using:
ng update @angular/core@22 @angular/cli@22
For an Nx workspace, first check which Nx version officially supports Angular 22 and use the Nx migration tooling:
nx migrate latest pnpm install nx migrate --run-migrations
Before upgrading, pay particular attention to:
For larger applications, I recommend performing the migration on a dedicated branch and running all unit, integration and end-to-end tests before merging it.
This is not everything Angular 22 brings.
Check out the official Angular 22 announcement, Angular 22 release page, Angular release schedule, Angular changelog, Angular CLI changelog and Angular Update Guide for the complete overview and migration instructions.
Angular 22 feels like a release that turns several promising ideas into a solid production foundation.
Signal Forms, Angular Aria, resource() and httpResource() are no longer experimental APIs that we can only try in demos. They are now stable tools that we can seriously consider for production applications.
At the same time, OnPush becoming the default shows that Angular is committed to performance by default. The new template features reduce boilerplate, while @Service() and injectAsync() make dependency injection more expressive and introduce new code-splitting possibilities.
I am especially excited about stable Signal Forms and asynchronous resources. Together, they make Angular applications feel much more signal-driven from end to end.
The move to one major release per year should also give teams more breathing room. Angular can continue evolving through regular minor releases, while major migrations become easier to schedule and manage.
If you are currently using Angular 21, upgrading to Angular 22 is definitely worth considering.