Modulo 04 — Data fetching SSR + Transfer State

Modulo 4 di 12 · Durata stimata: 60 minuti · Lab GitHub: ✓ (coming soon)


Introduzione

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.

Il problema: doppio fetch in 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:

  1. Server: Renderizza ArticleListComponent
  2. HTML inviato al client con lista articoli già renderizzata
  3. Client idrata la pagina

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).

Transfer State: concetto fondamentale

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)

Implementazione: makeStateKey + set/get

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:

Pattern avanzato: service wrapper con Transfer State

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:

HttpClient in SSR: withFetch() provider

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.

Timeout: protezione da richieste infinite

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
  }
}

Error handling: diverse strategie server vs client

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); } } }

Cookies e autenticazione in SSR

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.

Caching: evitare troppi fetch

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 invalidation: quando aggiornare?

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;
  }
}

Resolver pattern: prefetch data pre-rendering

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:

Lab esercizio: API client con Transfer State

Obbiettivo: Crea un servizio che fetcha dati da API con Transfer State, cache, timeout, e error handling.

Requisiti:

  1. Service `DataService` con metodo `getData()
  2. Transfer State per evitare doppio fetch
  3. Timeout 10 secondi
  4. Cache in memory (signal)
  5. Error handling: ritorna array vuoto su errore
  6. Componente che usa il service
  7. Zero crash server, funziona SSR

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

Recap: cosa sai adesso

Fine dei moduli free

Hai completato i 4 moduli gratuiti dell'Angular SSR Masterclass!

Moduli 1-4 (completati):

  1. ✅ Introduzione a Angular SSR (CSR vs SSR vs SSG)
  2. ✅ Setup @angular/ssr + server.ts
  3. ✅ Standalone components + signals server-side
  4. ✅ Data fetching SSR + Transfer State (tu sei qui)

Moduli 5-12 (gated — iscriviti per accesso):

  1. 📦 Prerendering + @defer per contenuto statico
  2. 📦 State management SSR (NgRx, Akita)
  3. 📦 SEO avanzato (structured data, sitemaps, robots, hreflang)
  4. 📦 Performance tuning (bundle splitting, lazy routes, lazy preloading)
  5. 📦 Caching strategies (Redis server-side, HTTP cache header, CDN)
  6. 📦 Deploy production (AWS EC2, Vercel, Heroku, GCP Cloud Run)
  7. 📦 Monorepo enterprise (NX, shared libraries, module federation)
  8. 📦 Migrazione da CSR a SSR (real-world case study: portfolio.dev)

Prossimi passi

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.