Angular 20: Stable Signal APIs, httpResource and the New File Naming
In Angular 20, effect, toSignal and linkedSignal become stable, resource swaps request for params, httpResource arrives, app.ts replaces app.component.ts and *ngIf gets deprecated.
Version note: This post covers what Angular 20 introduced; the code targets Angular 20.
The Signal World Grows Up
In Angular 19 we saw standalone become the default and met linkedSignal and the experimental resource(). Angular 20, released on May 28, 2025, is more of a consolidation release: most of the APIs that were still being tried out become stable, the resource API gets renamed parameters, and the files the CLI generates look completely different for the first time.
In this post we go through the signal APIs that became stable, the change on the resource side, httpResource, the new file naming, what happens to the old structural directives, where zoneless stands, and a few small additions to template expressions.
Signal APIs That Became Stable
In Angular 12, showing a clock that updates every second looked like this:
export class ClockComponent implements OnInit, OnDestroy {
now = new Date();
private sub?: Subscription;
ngOnInit(): void {
this.sub = interval(1000).subscribe(() => this.now = new Date());
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}
Subscribe, keep the reference, and don't forget to unsubscribe when the component is destroyed. In Angular 20 the same job looks like this:
import { Component, effect } from '@angular/core';
import { DatePipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';
@Component({
selector: 'app-clock',
imports: [DatePipe],
template: `<p>{{ now() | date:'HH:mm:ss' }}</p>`
})
export class Clock {
protected readonly now = toSignal(
interval(1000).pipe(map(() => new Date())),
{ initialValue: new Date() }
);
constructor() {
effect(() => console.log('Saat:', this.now()));
}
}
toSignal subscribes to the observable for you and closes the subscription when the component is destroyed. effect runs every time a signal it reads changes. We have seen both before; the difference is that in Angular 20, effect(), toSignal(), toObservable() and linkedSignal() are now stable. Alongside them, afterRenderEffect(), afterNextRender() and PendingTasks became stable too. afterRender() was renamed to afterEveryRender().
You may have noticed the class is called Clock, not ClockComponent. I'll explain why in a moment.
In the Resource API, request Is Now params
The parameter I asked you to keep in the back of your mind in the previous post has a new name:
// Angular 19
usersResource = resource({
request: () => ({ sort: this.sort() }),
loader: ({ request }) => fetchUsers(request.sort)
});
// Angular 20
usersResource = resource({
params: () => ({ sort: this.sort() }),
loader: ({ params }) => fetchUsers(params.sort)
});
The logic is the same, the name is just more descriptive: this function doesn't produce a request, it produces the parameters handed to loader. On the rxResource side, loader was renamed to stream:
usersResource = rxResource({
params: () => this.sort(),
stream: ({ params }) => this.http.get<User[]>(`/api/users?sort=${params}`)
});
Also, the reload() method now exists only on writable resources. Keep in mind that the resource APIs are still experimental in Angular 20. This rename shows exactly what the "experimental" label means: the API can change shape based on feedback.
httpResource: HttpClient as a Signal
In Angular 12, fetching a user profile meant taking HttpClient in the constructor and subscribing in ngOnInit:
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.http.get<User>(`/api/users/${this.userId}`)
.subscribe(user => this.user = user);
}
httpResource, which arrived in 19.2 and is still experimental in Angular 20, brings this down to one line:
import { Component, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
template: `
@if (user.value(); as u) {
<h3>{{ u.name }}</h3>
<p>{{ u.email }}</p>
} @else if (user.isLoading()) {
<p>Yükleniyor...</p>
} @else if (user.error()) {
<p>Kullanıcı bulunamadı</p>
}
`
})
export class UserProfile {
readonly userId = input.required<number>();
protected readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
}
You give httpResource a function that returns a URL. Because the function reads the userId signal, the request repeats on its own when the parent sends a new id. In Angular 20 you must pass the URL through a function like this, not as a plain string.
Since HttpClient runs underneath, your interceptors stay in play; all you need is provideHttpClient() in app.config.ts. It is a perfect fit for reading data. For saving and deleting, you keep using HttpClient directly.
New File Names: app.component.ts Out, app.ts In
With Angular 20 the official style guide got a major update, and the most visible change is in file names. In Angular 12, ng g c product-card produced this:
CREATE src/app/product-card/product-card.component.css
CREATE src/app/product-card/product-card.component.html
CREATE src/app/product-card/product-card.component.spec.ts
CREATE src/app/product-card/product-card.component.ts
UPDATE src/app/app.module.ts
In Angular 20, the same command outputs:
CREATE src/app/product-card/product-card.css
CREATE src/app/product-card/product-card.html
CREATE src/app/product-card/product-card.spec.ts
CREATE src/app/product-card/product-card.ts
.component is gone from the file names, and the class is now ProductCard instead of ProductCardComponent. There is no app.module.ts update either, because there is no module to update. In a new project the root component lives in app.ts, app.html and app.css, and it is called App.
The same rule applies to directives and services: ng g s user now generates a class named User in user.ts. Pipes, guards, interceptors and resolvers keep their class suffix, but the file name uses a dash instead of a dot: FromNowPipe inside from-now-pipe.ts.
Your existing project is not affected. The migration that runs during ng update adds settings to angular.json that keep the old naming. If you want to generate a single component the old way, there is the --type=component option. Among the guide's other recommendations: make properties used only in the template protected, and make fields that Angular initializes, such as input() and output(), readonly. You can see both in the examples above.
*ngIf, *ngFor and *ngSwitch Are Deprecated
Throughout Block A, our templates looked like this:
<p *ngIf="tasks.length === 0">Görev yok</p>
<li *ngFor="let task of tasks; trackBy: trackById">{{ task.title }}</li>
With the built-in control flow we saw earlier, the equivalent is:
@for (task of tasks; track task.id) {
<li>{{ task.title }}</li>
} @empty {
<p>Görev yok</p>
}
With Angular 20, NgIf, NgFor and NgSwitch are officially deprecated. Deprecated does not mean "removed"; it means "this will go away in a future major version, so move to the new way now." Your existing code keeps working; in fact, even in Angular 22, current at the time of writing, these directives are still there. Other structural directives such as ngTemplateOutlet are not part of this decision.
No need to convert them one by one; Angular ships a migration for this:
ng generate @angular/core:control-flow
Where Zoneless Stands
Zoneless change detection, which we met as an experiment in Angular 18, moved to developer preview in Angular 20.0, and the word "Experimental" dropped out of the provider's name:
bootstrapApplication(App, {
providers: [
provideZonelessChangeDetection(),
provideBrowserGlobalErrorListeners()
]
});
ng new now asks whether you want zoneless, and the flag is now called --zoneless. The story doesn't end there: with 20.2, released in August 2025, zoneless became stable. So within Angular 20's lifetime, zoneless went from "you can try it" to "you can use it in production with confidence."
What's New in Template Expressions
Templates moved a bit closer to TypeScript as well:
<p>{{ 2 ** 10 }}</p>
@if ('admin' in permissions) {
<a routerLink="/admin">Yönetim</a>
}
<button (click)="void save()">Kaydet</button>
<p>{{ `Merhaba ${name}` }}</p>
** for exponentiation, in to check whether an object has a key, and void to ignore a function's return value all arrived in Angular 20. void is small but useful: if an event handler accidentally returns false, the browser's default behavior is prevented, and void stops that from happening. Template literal support, shown on the last line, arrived in 19.2; Angular 20 added tagged template literals on top of it.
Moving to Angular 20: ng update
ng update @angular/core@20 @angular/cli@20
This version drops Node.js 18. Angular 20 needs Node from one of the ^20.19.0, ^22.12.0 or ^24.0.0 lines. On the TypeScript side, at least 5.8 is required.
Be ready for a few breaking changes after the update: TestBed.get() and InjectFlags were removed, the DOCUMENT token moved to the @angular/core package, and the @angular/platform-browser-dynamic package was deprecated. The migration handles most of the import changes itself. If you used resource in your code, review the request and loader lines separately.
A Small Experiment
Create a new project with npx @angular/cli@20 new deneme and look at the src/app folder first: there is no app.module.ts, and app.ts sits where app.component.ts used to be. Then run ng g c user-profile and check the generated file and class names.
Add provideHttpClient() to app.config.ts. Write the UserProfile component as above, using https://jsonplaceholder.typicode.com/users/${this.userId()} as the URL. Keep an id signal in App, increase and decrease it with two buttons, and pass it down with [userId]="id()". Finally, write a small template using *ngIf and *ngFor and watch how ng generate @angular/core:control-flow converts it.
While creating the project, the CLI asked whether you wanted zoneless; in Angular 20 that is still a choice. Next up is Angular 21, where zoneless becomes the default, Signal Forms arrive as an experiment, and Vitest takes Karma's place.