Angular 20: Real-World Auth & Data Patterns with rxResource, Tailwind v4 & daisyUI 5
Introduction
Angular 20 introduces the new build system (@angular/build), Tailwind v4 zero-config integration, and a refined Resource API for reactive data using Signals — replacing request/loader with params/stream.
This article explores a real-world auth-enabled store app (Teslo | Shop) using:
- Functional guards (
CanMatchFn) - Signal-based
AuthServicewithrxResource - Tailwind v4 + daisyUI 5 styling
- Modern Angular 20 templates with
@if/@for
1️⃣ Auth Service with Signals + rxResource
import { HttpClient } from '@angular/common/http';
import { computed, inject, Injectable, signal } from '@angular/core';
import { environment } from 'src/environments/environment';
import { catchError, map, Observable, of, tap } from 'rxjs';
import { rxResource } from '@angular/core/rxjs-interop';
import { AuthResponse } from '@auth/interfaces/auth-response.interface';
import { User } from '@auth/interfaces/user.interface';
type AuthStatus = 'checking' | 'authenticated' | 'not-authenticated';
const baseUrl = environment.baseUrl;
@Injectable({ providedIn: 'root' })
export class AuthService {
private _authStatus = signal<AuthStatus>('checking');
private _user = signal<User | null>(null);
private _token = signal<string | null>(localStorage.getItem('token'));
private http = inject(HttpClient);
checkStatusResource = rxResource({
stream: () => this.checkStatus(),
});
authStatus = computed<AuthStatus>(() => {
if (this._authStatus() === 'checking') return 'checking';
if (this._user()) {
return 'authenticated';
}
return 'not-authenticated';
});
user = computed(() => this._user());
token = computed(this._token);
isAdmin = computed(() => this._user()?.roles.includes('admin') ?? false);
login(email: string, password: string): Observable<boolean> {
return this.http
.post<AuthResponse>(`${baseUrl}/auth/login`, {
email: email,
password: password,
})
.pipe(
map((resp) => this.handleAuthSuccess(resp)),
catchError((error: any) => this.handleAuthError(error))
);
}
checkStatus(): Observable<boolean> {
const token = localStorage.getItem('token');
if (!token) {
this.logout();
return of(false);
}
return this.http
.get<AuthResponse>(`${baseUrl}/auth/check-status`, {
// headers: {
// Authorization: `Bearer ${token}`,
// },
})
.pipe(
map((resp) => this.handleAuthSuccess(resp)),
catchError((error: any) => this.handleAuthError(error))
);
}
logout() {
this._user.set(null);
this._token.set(null);
this._authStatus.set('not-authenticated');
localStorage.removeItem('token');
}
private handleAuthSuccess({ token, user }: AuthResponse) {
this._user.set(user);
this._authStatus.set('authenticated');
this._token.set(token);
localStorage.setItem('token', token);
return true;
}
private handleAuthError(error: any) {
this.logout();
return of(false);
}
}
2️⃣ Functional Guards in Angular 20
import { inject } from '@angular/core';
import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router';
import { AuthService } from '@auth/services/auth.service';
import { firstValueFrom } from 'rxjs';
export const IsAdminGuard: CanMatchFn = async (
route: Route,
segments: UrlSegment[]
) => {
const authService = inject(AuthService);
await firstValueFrom(authService.checkStatus());
return authService.isAdmin();
};
Angular 20 favors function-based guards — concise, tree-shakable, and perfect for Signals.
3️⃣ HTTP Interceptor for Tokens
import { HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '@auth/services/auth.service';
export function authInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn
) {
const token = inject(AuthService).token();
const newReq = req.clone({
headers: req.headers.append('Authorization', `Bearer ${token}`),
});
return next(newReq);
}
4️⃣ Login Page (Reactive Form + Signals)
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { AuthService } from '@auth/services/auth.service';
@Component({
selector: 'app-login-page',
imports: [RouterLink, ReactiveFormsModule],
templateUrl: './login-page.component.html',
})
export class LoginPageComponent {
fb = inject(FormBuilder);
hasError = signal(false);
isPosting = signal(false);
router = inject(Router);
authService = inject(AuthService);
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
});
onSubmit() {
if (this.loginForm.invalid) {
this.hasError.set(true);
setTimeout(() => {
this.hasError.set(false);
}, 2000);
return;
}
const { email = '', password = '' } = this.loginForm.value;
this.authService.login(email!, password!).subscribe((isAuthenticated) => {
if (isAuthenticated) {
this.router.navigateByUrl('/');
return;
}
this.hasError.set(true);
setTimeout(() => {
this.hasError.set(false);
}, 2000);
});
}
}
5️⃣ Navbar with Auth-Driven UI
<div class="navbar bg-base-100">
<div class="navbar-start">
<a routerLink="/" class="btn btn-ghost text-xl font-montserrat">
Teslo<span class="text-secondary">|Shop</span>
</a>
</div>
<div class="navbar-end gap-4">
@if (authService.authStatus() === 'authenticated') {
<button class="btn btn-ghost">{{ authService.user()?.fullName }}</button>
<button class="btn btn-sm btn-error" (click)="authService.logout()">Salir</button>
}
@else if (authService.authStatus() === 'not-authenticated') {
<a routerLink="/auth/login" class="btn btn-secondary">Login</a>
}
@else {
<a class="btn btn-ghost">...</a>
}
</div>
</div>
6️⃣ Admin Layout (Tailwind v4 + daisyUI)
<div class="bg-slate-800 h-screen text-slate-300">
<div class="flex">
<aside class="bg-gray-900 w-64 h-screen p-6">
<h1 class="text-2xl font-bold">Teslo<span class="text-blue-500">|Shop</span></h1>
<p class="text-slate-500 text-sm">Welcome, {{ user()?.fullName }}</p>
<a routerLink="/admin/products" class="block py-3 hover:bg-white/5">Products</a>
<button class="btn btn-ghost text-accent w-full mt-6" (click)="authService.logout()">Log out</button>
</aside>
<main class="flex-1 p-5 overflow-y-auto"><router-outlet /></main>
</div>
</div>
7️⃣ Migration Notes (v19 → v20)
| Old | New | Description |
|---|---|---|
request |
params |
reactive fetch input |
loader |
stream |
observable/promise factory |
ResourceStatus |
`'idle' | 'loading' |
{% raw %}@if/@for
|
new control flow | built-in syntax |
💡 Expert Takeaways
-
rxResource<T, P>gives typed results, cancelable requests, and easy reloads. - Provide
defaultValueto avoidundefinedguards. - HttpClient supports
{ signal: abortSignal }. - Functional guards reduce boilerplate.
- Tailwind v4 + daisyUI v5 bring modern UI theming.
📚 Resources
Next up: Streaming Data with rxResource.stream() and SSE in Angular 20 🚀

Top comments (0)