Angular 19 Signals and Resource API: Build Reactive Web Apps in 2026

Angular 19 changes everything you thought you knew about the framework. Signals replace RxJS for state, the Resource API turns async data into first-class reactive primitives, and zoneless change detection delivers React-level performance. In this tutorial you will build a complete reactive dashboard and master the modern Angular stack from day one.
What You Will Learn
By the end of this tutorial, you will be able to:
- Create standalone components without NgModules
- Use Signals (
signal,computed,effect) for reactive state - Fetch remote data with the new Resource API and
httpResource - Run Angular in zoneless mode for maximum performance
- Build signal-based forms with type safety
- Bridge RxJS observables and Signals with
toSignalandtoObservable - Deploy a production-ready Angular app
Prerequisites
Before starting, ensure you have:
- Node.js 20+ (Angular 19 requires Node 20 or later)
- Basic TypeScript knowledge (interfaces, generics, decorators)
- Familiarity with HTML and CSS
- A code editor, VS Code with the Angular Language Service extension recommended
- Angular CLI experience is helpful but not mandatory
Why Angular 19 Matters
For years, Angular felt heavy compared to React or Vue. NgModules required boilerplate, Zone.js patched globals to trigger change detection, and RxJS was the only idiomatic way to handle async. Angular 19 fixes all of this:
- Standalone by default — no NgModules, cleaner imports
- Signals — fine-grained reactivity without Zone.js overhead
- Zoneless change detection — smaller bundles, faster rendering
- Resource API — reactive data fetching with loading and error states built in
- Incremental hydration — server-rendered apps that hydrate on demand
The result is an Angular that feels modern, lightweight, and a genuine alternative to React Server Components.
What You Will Build
A real-time Signals Dashboard that displays GitHub repository statistics. The app will:
- Fetch repos from the GitHub API using
httpResource - Filter and sort them reactively with Signals
- Persist user preferences with a Signal-based store
- Update the UI instantly with zero
ngIforngForboilerplate, using the new@ifand@forcontrol flow
Step 1: Create the Angular 19 Project
Install the latest Angular CLI and generate a new project:
npm install -g @angular/cli@19
ng new signals-dashboard --style=css --ssr=false --standalone
cd signals-dashboardOpen angular.json and confirm "strict": true under the TypeScript options. Angular 19 ships with strict mode on by default, which pairs nicely with Signals.
Now enable zoneless change detection. Edit src/main.ts:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection(),
provideHttpClient(),
],
}).catch((err) => console.error(err));Delete zone.js from polyfills in angular.json to shave around 40 KB from the bundle.
Step 2: Understand the Three Core Signal Primitives
Signals are reactive values that notify their consumers when they change. Angular exposes three primitives:
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const double = computed(() => count() * 2);
effect(() => {
console.log(`count is ${count()}, double is ${double()}`);
});
count.set(5);
count.update((n) => n + 1);Three things to notice:
- Read with
count(), like calling a function. - Write with
.set()or.update()on a writable signal. - Derive with
computed(), which memoizes automatically.
effect() runs whenever any read signal changes. Effects are cleaned up when their owning component is destroyed — no manual unsubscribe.
Step 3: Build a Signal-Based Store
Create src/app/stores/preferences.store.ts:
import { Injectable, signal, computed, effect } from '@angular/core';
export type SortBy = 'stars' | 'updated' | 'name';
@Injectable({ providedIn: 'root' })
export class PreferencesStore {
readonly query = signal('');
readonly sortBy = signal<SortBy>('stars');
readonly minStars = signal(0);
readonly activeFiltersCount = computed(() => {
let count = 0;
if (this.query().length > 0) count++;
if (this.minStars() > 0) count++;
return count;
});
constructor() {
const saved = localStorage.getItem('prefs');
if (saved) {
const parsed = JSON.parse(saved);
this.query.set(parsed.query ?? '');
this.sortBy.set(parsed.sortBy ?? 'stars');
this.minStars.set(parsed.minStars ?? 0);
}
effect(() => {
const snapshot = {
query: this.query(),
sortBy: this.sortBy(),
minStars: this.minStars(),
};
localStorage.setItem('prefs', JSON.stringify(snapshot));
});
}
reset(): void {
this.query.set('');
this.sortBy.set('stars');
this.minStars.set(0);
}
}This is a complete reactive store in roughly 30 lines. No Redux, no NgRx boilerplate, no Observable pipelines. The effect() call silently syncs every change to localStorage.
Step 4: Fetch Data with the Resource API
The Resource API is Angular 19's answer to loading and error states. Create src/app/services/repos.service.ts:
import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { PreferencesStore } from '../stores/preferences.store';
export interface Repo {
id: number;
name: string;
full_name: string;
description: string | null;
stargazers_count: number;
updated_at: string;
html_url: string;
}
@Injectable({ providedIn: 'root' })
export class ReposService {
private readonly prefs = inject(PreferencesStore);
readonly reposResource = httpResource<Repo[]>(() => {
const q = this.prefs.query().trim();
if (!q) return undefined;
return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
});
readonly filteredRepos = computed(() => {
const repos = this.reposResource.value() ?? [];
const minStars = this.prefs.minStars();
const sortBy = this.prefs.sortBy();
const filtered = repos.filter((r) => r.stargazers_count >= minStars);
return [...filtered].sort((a, b) => {
if (sortBy === 'stars') return b.stargazers_count - a.stargazers_count;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return b.updated_at.localeCompare(a.updated_at);
});
});
}httpResource takes a function returning a URL. When the function re-evaluates because one of its reactive dependencies changed, Angular automatically fetches the new data and exposes value(), status(), and error() signals.
Returning undefined from the URL factory pauses the request, a clean way to avoid fetching when the user has not typed anything yet.
Step 5: Modern Control Flow with @if and @for
The legacy structural directives *ngIf and *ngFor are replaced with a block syntax that parses faster and reads more naturally. Create src/app/components/repo-list.component.ts:
import { Component, inject } from '@angular/core';
import { ReposService } from '../services/repos.service';
@Component({
selector: 'app-repo-list',
standalone: true,
template: `
@if (repos.reposResource.status() === 'loading') {
<p class="loading">Loading repositories...</p>
} @else if (repos.reposResource.error()) {
<p class="error">Failed to load: {{ repos.reposResource.error()?.message }}</p>
} @else {
@for (repo of repos.filteredRepos(); track repo.id) {
<article class="repo-card">
<h3>
<a [href]="repo.html_url" target="_blank">{{ repo.full_name }}</a>
</h3>
@if (repo.description) {
<p>{{ repo.description }}</p>
}
<div class="meta">
<span>★ {{ repo.stargazers_count }}</span>
<span>Updated {{ repo.updated_at | date: 'mediumDate' }}</span>
</div>
</article>
} @empty {
<p class="empty">No repositories match your filters.</p>
}
}
`,
styleUrl: './repo-list.component.css',
})
export class RepoListComponent {
readonly repos = inject(ReposService);
}Key improvements over legacy Angular:
@emptyblock handles empty collections cleanlytrackis required, which prevents the commontrackBybug- The compiler catches syntax errors at build time rather than runtime
Step 6: Build a Signal-Based Search Bar
Signal-based forms are stable in Angular 19. Create src/app/components/search-bar.component.ts:
import { Component, inject } from '@angular/core';
import { PreferencesStore } from '../stores/preferences.store';
@Component({
selector: 'app-search-bar',
standalone: true,
template: `
<div class="search-bar">
<input
type="search"
placeholder="Search GitHub repos..."
[value]="prefs.query()"
(input)="onQueryChange($event)"
/>
<select
[value]="prefs.sortBy()"
(change)="onSortChange($event)"
>
<option value="stars">Most stars</option>
<option value="updated">Recently updated</option>
<option value="name">Name A to Z</option>
</select>
<input
type="number"
min="0"
placeholder="Min stars"
[value]="prefs.minStars()"
(input)="onMinStarsChange($event)"
/>
@if (prefs.activeFiltersCount() > 0) {
<button (click)="prefs.reset()">
Reset ({{ prefs.activeFiltersCount() }})
</button>
}
</div>
`,
})
export class SearchBarComponent {
readonly prefs = inject(PreferencesStore);
onQueryChange(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.prefs.query.set(value);
}
onSortChange(event: Event): void {
const value = (event.target as HTMLSelectElement).value as 'stars' | 'updated' | 'name';
this.prefs.sortBy.set(value);
}
onMinStarsChange(event: Event): void {
const value = Number((event.target as HTMLInputElement).value);
this.prefs.minStars.set(Number.isFinite(value) ? value : 0);
}
}When the user types, the query signal updates. The httpResource factory re-evaluates and refetches. The filteredRepos computed signal recalculates and the template re-renders, all without calling markForCheck or injecting ChangeDetectorRef.
Step 7: Wire Everything into the Root Component
Edit src/app/app.component.ts:
import { Component } from '@angular/core';
import { SearchBarComponent } from './components/search-bar.component';
import { RepoListComponent } from './components/repo-list.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [SearchBarComponent, RepoListComponent],
template: `
<header>
<h1>Angular 19 Signals Dashboard</h1>
</header>
<main>
<app-search-bar />
<app-repo-list />
</main>
`,
})
export class AppComponent {}Run the app with ng serve and open http://localhost:4200. Type a search term, adjust the star filter, and watch the list update instantly without a single manual subscription.
Step 8: Debounce User Input with RxJS Interop
Sometimes you still want Observable ergonomics, for example to debounce a search input. Angular exposes toSignal and toObservable for seamless interop.
Update ReposService to debounce the query:
import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';
import { PreferencesStore } from '../stores/preferences.store';
@Injectable({ providedIn: 'root' })
export class ReposService {
private readonly prefs = inject(PreferencesStore);
private readonly debouncedQuery = toSignal(
toObservable(this.prefs.query).pipe(
debounceTime(300),
distinctUntilChanged(),
),
{ initialValue: '' }
);
readonly reposResource = httpResource<{ items: Repo[] }>(() => {
const q = this.debouncedQuery().trim();
if (!q) return undefined;
return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
});
}This is the pattern the Angular team recommends for hybrid codebases: use Signals for state, reach for RxJS only when you need operators like debounceTime, switchMap, or retryWhen.
Step 9: Test Signal-Based Code
Signals are pure functions, which makes them trivially testable. Create preferences.store.spec.ts:
import { TestBed } from '@angular/core/testing';
import { PreferencesStore } from './preferences.store';
describe('PreferencesStore', () => {
let store: PreferencesStore;
beforeEach(() => {
localStorage.clear();
TestBed.configureTestingModule({});
store = TestBed.inject(PreferencesStore);
});
it('starts with default values', () => {
expect(store.query()).toBe('');
expect(store.sortBy()).toBe('stars');
expect(store.activeFiltersCount()).toBe(0);
});
it('computes active filter count', () => {
store.query.set('angular');
store.minStars.set(100);
expect(store.activeFiltersCount()).toBe(2);
});
it('resets all preferences', () => {
store.query.set('test');
store.reset();
expect(store.query()).toBe('');
});
});No fakeAsync, no TestScheduler, no done callbacks. Set a signal, read another, assert. This simplicity is the strongest argument for moving to Signals.
Step 10: Build and Deploy
Produce an optimized build:
ng build --configuration productionThe dist/signals-dashboard/browser folder contains static assets ready for any CDN. For Vercel or Netlify, the default output works out of the box. For a traditional VPS, serve index.html with a fallback to handle client-side routing.
Check the bundle size:
npx source-map-explorer dist/signals-dashboard/browser/main-*.jsA zoneless Angular 19 app lands around 110 KB gzipped for this dashboard, comparable to a React + React Query project of similar complexity.
Testing Your Implementation
Walk through this checklist:
- The app loads without errors in the console
- Typing in the search box fetches data after a 300 ms debounce
- The sort dropdown reorders results instantly
- Setting a minimum star filter hides repos under the threshold
- Refreshing the page restores your last query, sort, and filter
- The loading state shows for slow requests
- Disabling your network throws a visible error, not a silent failure
Troubleshooting
NG0203: inject() must be called from an injection context
You are calling inject() outside a constructor, field initializer, or factory function. Move it into the constructor or use a field initializer.
Error: NG02200: Cannot find a differ supporting object
A @for block is missing track. Add track item.id or whatever unique key identifies each item.
Resource keeps refetching on every render
Your URL factory depends on a value that changes on every change detection cycle. Wrap the offending dependency in a computed() to stabilize it.
Hot reload breaks after adding Signals
Older versions of the Angular CLI cache aggressively. Run rm -rf .angular and restart ng serve.
Next Steps
- Add route-level data loading with the Resource API inside
resolveguards - Enable incremental hydration for server-rendered pages with
@defer - Explore NgRx SignalStore, which builds a feature-complete store on top of Signals
- Integrate TanStack Query for Angular for advanced caching and background refetching
- Read the official Angular Signals guide for edge cases around effects
You may also enjoy these related tutorials:
- Vite 6, React, TypeScript modern web app
- Biome, replace ESLint and Prettier with one tool
- TanStack Query v5 for Next.js data fetching
Conclusion
Angular 19 is a legitimate modern framework again. Signals give you fine-grained reactivity without the overhead of Zone.js, the Resource API reduces async boilerplate to almost nothing, and standalone components finally let you write Angular without a module in sight.
If you evaluated Angular five years ago and walked away, this is the release that deserves a second look. Teams with a large Angular codebase get a clear incremental migration path. Teams starting fresh get a batteries-included framework with first-class TypeScript, dependency injection, and a sustainable long-term release cadence.
The dashboard you just built is the skeleton of any serious Angular 19 app: reactive state, typed data fetching, debounced input, and clean testing. Clone it, extend it, and ship your next project the modern Angular way.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Vite 6 + React + TypeScript: Build a Modern Web App from Scratch in 2026
Master Vite 6 with React and TypeScript — from project creation to deployment. This comprehensive guide covers the Environment API, lightning-fast HMR, optimized bundling, testing with Vitest, and production best practices.

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.