Modulo 4 di 12 · Durata stimata: 60 minuti · Lab GitHub: ✓ (coming soon)
Uno dei maggiori problemi SSR è questo: come evitare che il client faccia la stessa HTTP request del server? Se il server fetch articoli dal backend e li invia come HTML, il client non dovrebbe ri-fetch gli stessi dati (sarebbe doppio lavoro, lento, e consuma bandwidth).
Questo modulo insegna il pattern Transfer State di Angular: meccanismo per passare dati dal server al client senza re-fetch. Vedrai anche caching, timeout, error handling, e cookies con SSR.
Scenario senza Transfer State:
Una pagina lista articoli. Componente ArticleListComponent fetch da `/api/articles`.
// article-list.component.ts
export class ArticleListComponent implements OnInit {
articles = signal([]);
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('/api/articles').subscribe((data) => {
this.articles.set(data);
});
}
template = `
{{ article.title }}
`;
}
Flusso SSR senza Transfer State:
Problema: Due HTTP request identiche. Lento, spreca banda, aumenta latenza percezione client, sovraccarica backend.
Soluzione: Transfer State — Includi i dati nella prima richiesta HTML, il client li legge da una variabile globale (non fa HTTP).
Angular fornisce un servizio `TransferState` per salvare dati durante rendering server e leggerli client:
// Backend (server.ts render)
commonEngine.render({
...
providers: [
// Transfer State è iniettato automaticamente
]
});
// Durante render:
// - Componente esegue, salva dati in TransferState
// - Dati serializzati in window.__TRANSFER_STATE__ (JSON window globale)
// - HTML + JSON inviato al client
// Client (idratazione):
// - Browser legge window.__TRANSFER_STATE__
// - TransferState popola lo stesso servizio
// - Componente legge da TransferState (non fa HTTP)
Step 1: Crea una chiave univoca per i dati
// article.keys.ts
import { makeStateKey } from '@angular/core';
export const ARTICLES_KEY = makeStateKey('articles');
Step 2: Salva i dati nel server
// article-list.component.ts (server-side rendering)
import { Component, inject, PLATFORM_ID } from '@angular/core';
import { TransferState } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { isPlatformServer } from '@angular/common';
import { ARTICLES_KEY } from './article.keys';
@Component({
selector: 'app-article-list',
standalone: true,
template: `
{{ article.title }}
`,
})
export class ArticleListComponent implements OnInit {
private transferState = inject(TransferState);
private http = inject(HttpClient);
private platformId = inject(PLATFORM_ID);
articles = signal([]);
ngOnInit() {
// Controlla se i dati sono già stati trasferiti (da server)
if (this.transferState.hasKey(ARTICLES_KEY)) {
// Client: leggi da Transfer State (NON fare HTTP)
const articles = this.transferState.get(ARTICLES_KEY, []);
this.articles.set(articles);
} else if (isPlatformServer(this.platformId)) {
// Server: fetch dai dati, salva in Transfer State
this.http.get('/api/articles').subscribe((data) => {
this.articles.set(data);
this.transferState.set(ARTICLES_KEY, data); // ← IMPORTANTE
});
} else {
// Client (fallback, Transfer State non trovato): fetch normalmente
this.http.get('/api/articles').subscribe((data) => {
this.articles.set(data);
});
}
}
}
Risultato:
Il pattern sopra è verboso. Meglio creare un servizio che gestisce Transfer State automaticamente:
// article.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/core';
import { firstValueFrom } from 'rxjs';
const ARTICLES_KEY = makeStateKey('articles');
@Injectable({
providedIn: 'root',
})
export class ArticleService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
async getArticles(): Promise {
// Step 1: Controlla Transfer State
if (this.transferState.hasKey(ARTICLES_KEY)) {
return this.transferState.get(ARTICLES_KEY, []);
}
// Step 2: Fetch da API (server esegue questo, client no)
const articles = await firstValueFrom(
this.http.get('/api/articles')
);
// Step 3: Salva in Transfer State per il client
this.transferState.set(ARTICLES_KEY, articles);
return articles;
}
}
Utilizzo semplice nel componente:
@Component({
selector: 'app-article-list',
standalone: true,
template: `
{{ article.title }}
`,
})
export class ArticleListComponent implements OnInit {
private articleService = inject(ArticleService);
articles = signal([]);
async ngOnInit() {
const data = await this.articleService.getArticles();
this.articles.set(data);
}
}
Vantaggi:
In Node.js (server-side), HttpClient di default non funziona (no `fetch` nativo). Angular 18+ fornisce il provider `withFetch()` per abilitarlo:
// app.config.ts
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()), // ← IMPORTANTE per SSR
],
};
Cosa fa: Abilita fetch nativo in Node.js (disponibile da Node 18+). Senza questo, HttpClient non può fare richieste server-side.
Alternativa pre-Node18: Usare un polyfill come `node-fetch`, ma `withFetch()` è nativo e più efficiente nel 2026.
In SSR, se una richiesta HTTP non finisce, il rendering server si blocca. Angular @angular/ssr ha timeout default ~30 secondi, ma è saggio essere più aggressivo:
// article.service.ts con timeout
import { timeout } from 'rxjs/operators';
export class ArticleService {
private http = inject(HttpClient);
async getArticles(): Promise {
const articles = await firstValueFrom(
this.http.get('/api/articles').pipe(
timeout(5000) // 5 secondi timeout
)
);
return articles;
}
}
Timeout fallback: Se HTTP fallisce, ritorna dati di default:
async getArticles(): Promise {
try {
const articles = await firstValueFrom(
this.http.get('/api/articles').pipe(
timeout(5000),
catchError(() => of([])) // Empty array se timeout/error
)
);
return articles;
} catch (err) {
console.error('Failed to fetch articles:', err);
return []; // Default vuoto
}
}
Server-side error: Se API non risponde durante SSR, cosa fai?
Esempio option C (partial success):
export class ArticleService {
async getArticles(): Promise<{ articles: Article[]; error?: string }> {
try {
const articles = await firstValueFrom(
this.http.get('/api/articles').pipe(timeout(5000))
);
return { articles };
} catch (err) {
// Partial failure: ritorna array vuoto + messaggio errore
return {
articles: [],
error: 'Articoli non disponibili al momento',
};
}
}
}
Nel componente:
@Component({
template: `
@if (articles().length > 0) {
{{ article.title }}
} @else if (error()) {
{{ error() }}
} @else {
No articles available
}
`,
})
export class ArticleListComponent {
articles = signal([]);
error = signal(null);
async ngOnInit() {
const result = await this.articleService.getArticles();
this.articles.set(result.articles);
if (result.error) {
this.error.set(result.error);
}
}
}
Problema: In SSR, il server fa richieste HTTP al backend. Come passa i cookie di autenticazione?
Scenari:
Soluzione A: Proxy interno (recommended per SSR):
Il server SSR fa da proxy: richiesta `/api/articles` → server SSR → backend `:8080/api/articles`. Cookie gestito dal server (HTTP-only).
// server.ts
app.use('/api', (req, res) => {
const backendUrl = `http://localhost:8080${req.path}`;
// Inoltra il cookie dal client al backend
const options: RequestInit = {
method: req.method,
headers: { ...req.headers },
credentials: 'include', // ← includi cookie
};
fetch(backendUrl, options)
.then((r) => r.text())
.then((html) => res.send(html));
});
Soluzione B: Token JWT in header (più comune nel 2026):
Backend rilascia JWT token. Server SSR lo include in `Authorization: Bearer token`.
// article.service.ts con JWT
export class ArticleService {
private http = inject(HttpClient);
private auth = inject(AuthService);
async getArticles(): Promise {
const token = this.auth.getToken(); // JWT from localStorage/sessionStorage
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`,
});
const articles = await firstValueFrom(
this.http.get('/api/articles', { headers }).pipe(
timeout(5000)
)
);
return articles;
}
}
In SSR: Server non ha localStorage (no browser). Soluzione: JWT salvato in memory server-side, oppure passato come env var.
Scenario: Tre componenti diverse richiedono `getArticles()`. Vuoi fetch UNA VOLTA, non tre.
Soluzione: RxJS shareReplay o signal cache:
// article.service.ts con cache
import { shareReplay } from 'rxjs/operators';
export class ArticleService {
private http = inject(HttpClient);
private cache$: Observable | null = null;
getArticles(): Observable {
if (!this.cache$) {
this.cache$ = this.http.get('/api/articles').pipe(
shareReplay(1) // Cache una volta, condividi con tutti i subscriber
);
}
return this.cache$;
}
// Invalidate cache se necessario
clearCache() {
this.cache$ = null;
}
}
Alternativa signal-based (più moderno):
export class ArticleService {
private http = inject(HttpClient);
private articleCache = signal(null);
async getArticles(): Promise {
if (this.articleCache() !== null) {
return this.articleCache()!;
}
const articles = await firstValueFrom(
this.http.get('/api/articles')
);
this.articleCache.set(articles);
return articles;
}
clearCache() {
this.articleCache.set(null);
}
}
Cache è utile ma richiede strategia per invalidare quando dati cambiano:
Esempio time-based:
export class ArticleService {
private cache = signal(null);
private cacheTime = signal(null);
private CACHE_DURATION = 5 * 60 * 1000; // 5 minuti
async getArticles(): Promise {
const now = Date.now();
const isCacheValid =
this.cache() !== null &&
this.cacheTime() !== null &&
now - this.cacheTime()! < this.CACHE_DURATION;
if (isCacheValid) {
return this.cache()!;
}
const articles = await firstValueFrom(
this.http.get('/api/articles')
);
this.cache.set(articles);
this.cacheTime.set(now);
return articles;
}
}
Angular fornisce `resolve` per prefetch dati PRIMA di attivare il componente:
// article.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ArticleService } from './article.service';
export const articlesResolver: ResolveFn = async () => {
const service = inject(ArticleService);
return service.getArticles();
};
// routing.ts
const routes: Routes = [
{
path: 'articles',
component: ArticleListComponent,
resolve: { articles: articlesResolver }, // ← fetch prima del componente
},
];
// article-list.component.ts
@Component({...})
export class ArticleListComponent {
route = inject(ActivatedRoute);
articles = signal([]);
constructor() {
// Dati già disponibili dal resolver
const data = this.route.snapshot.data['articles'];
this.articles.set(data);
}
}
Vantaggi SSR:
Obbiettivo: Crea un servizio che fetcha dati da API con Transfer State, cache, timeout, e error handling.
Requisiti:
API endpoint da mockare: `GET /api/data` → `{ items: [...] }`
Snippet starter:
// data.service.ts
export class DataService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private cache = signal- (null);
async getData(): Promise
- {
// Implementa Transfer State + cache qui
}
}
// data-list.component.ts
@Component({
selector: 'app-data-list',
standalone: true,
template: `
@for (item of items(); track item.id) {
{{ item.name }}
} @empty {
No items
}
`,
})
export class DataListComponent implements OnInit {
private dataService = inject(DataService);
items = signal- ([]);
async ngOnInit() {
this.items.set(await this.dataService.getData());
}
}
Code repo (coming soon): github.com/fedcal01/angular-ssr-masterclass-labs/tree/main/module-04
Hai completato i 4 moduli gratuiti dell'Angular SSR Masterclass!
Moduli 1-4 (completati):
Moduli 5-12 (gated — iscriviti per accesso):
Opzione 1: Iscriviti per i moduli 5-12
I moduli 5-12 coprono argomenti avanzati: prerendering, state management, SEO enterprise, performance, caching, deployment production, e monorepo. Sono riservati ai subscriber della newsletter.
→ Iscriviti alla newsletter per ricevere i moduli non appena pubblicati.
Opzione 2: Continua a praticare
Approfondisci gli esercizi lab dei moduli 1-4. Crea una piccola app completa SSR (portfolio, blog, lista prodotti). Pratica è il miglior insegnante.
Opzione 3: Consulenza
Se hai una app Angular esistente e vuoi migrare a SSR, contattami per consulenza. Posso aiutare con architettura, migrazione, debugging, performance tuning.
Grazie per aver seguito l'Angular SSR Masterclass. Sei pronto a rendere le tue app Angular veloci, SEO-friendly, e scalabili con server-side rendering.
Domande? Commenti? Feedback sui moduli? Apri una discussione su GitHub.