Modulo 02 — Setup @angular/ssr + server.ts

Modulo 2 di 12 · Durata stimata: 45 minuti · Lab GitHub: ✓ (coming soon)


Introduzione

In questo modulo vedrai come aggiungere @angular/ssr a un progetto Angular 21 esistente. Esploreremo l'anatomia del file `server.ts`, come funziona l'Express app integrata, e come debuggare i problemi comuni che emergono quando server-side rendering entra in gioco.

Questo modulo è pratico: avrai uno snippet di server.ts funzionante e saprai come avviare il tuo sito in SSR locally.

Prerequisiti

Step 1: Installare @angular/ssr via schematic

Angular fornisce uno schematic ufficiale per aggiungere SSR a un progetto esistente:

ng add @angular/ssr

Questo comando:

  1. Installa dipendenze: @angular/platform-server, @angular/ssr
  2. Crea server.ts: file Express nel root della directory
  3. Aggiorna angular.json: aggiunge configurazione SSR (builder, prerender)
  4. Crea zone.js compatibility: configura polyfill server-side
  5. Aggiorna package.json scripts: aggiunge `serve:ssr`, `prerender`, etc.

Output atteso (console):

✔ Packages installed successfully.
✔ New SSR-related files created.
✔ angular.json updated for SSR builds.

Your application is now ready for server-side rendering!
Run 'npm run build' and then 'npm run serve:ssr' to test locally.

Step 2: Capire la struttura angular.json SSR

Dopo `ng add @angular/ssr`, il tuo `angular.json` avrà tre builder per la stessa applicazione:

"projects": {
  "portfolio": {
    "architect": {
      "build": {
        "builder": "@angular-devkit/build-angular:browser",
        "options": {
          "outputPath": "dist/portfolio/browser",
          "index": "src/index.html",
          "main": "src/main.ts",
          ...
        }
      },
      "server": {
        "builder": "@angular-devkit/build-angular:server",
        "options": {
          "outputPath": "dist/portfolio/server",
          "main": "src/main.server.ts",
          ...
        }
      },
      "prerender": {
        "builder": "@angular-devkit/build-angular:prerender",
        "options": {
          "routes": ["src/prerender.routes.ts"],
          "numProcesses": 4
        }
      }
    }
  }
}

Cosa significa:

Flusso di build SSR:

npm run build
  └─ build browser (→ dist/portfolio/browser/)
  └─ build server (→ dist/portfolio/server/)
  └─ copia assets (→ dist/portfolio/browser/assets/)

npm run serve:ssr
  └─ avvia Express server (legge dist/portfolio/server/main.js)
  └─ per ogni richiesta HTTP:
       └─ carica il bundle server Angular
       └─ renderizza il componente a HTML
       └─ invia HTML + assets al browser

Step 3: Anatomia di server.ts

Questo è il cuore di SSR. Ecco una versione commentata basata su quella di federicocalo.dev:

// server.ts — Express app per SSR

import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';

// Ricava __dirname in ES modules
const filename = fileURLToPath(import.meta.url);
const dirname_path = dirname(filename);

// Express app
const app = express();

// Carica il bootstrap Angular lato server
const { AppServerModule } = await import(
  join(dirname_path, 'server', 'main.js')
);

// Configura CommonEngine (render SSR)
const commonEngine = new CommonEngine();

// === MIDDLEWARE ===

// 1. Servire static assets (CSS, JS, images)
//    Importante: deve andare PRIMA del catch-all route
app.use(
  express.static(join(dirname_path, 'browser'), {
    maxAge: '1y', // Browser cache 1 anno per immutable assets
    etag: false,  // Disabilita ETag se vuoi cache aggressivo
  })
);

// 2. Security headers (CSP, HSTS, X-Content-Type-Options)
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  );
  next();
});

// === ROUTING ===

// 1. Tutte le altre rotte (catch-all) → renderizza con SSR
app.get('*', (req, res) => {
  // La BaseHref deve corrispondere al basePath della tua app
  const baseUrl = `${req.protocol}://${req.get('host')}/`;

  // Renderizza il componente Angular a HTML
  commonEngine
    .render({
      bootstrap: AppServerModule,
      providers: [
        { provide: APP_BASE_HREF, useValue: req.baseUrl }, // URL corrente
      ],
      url: `${baseUrl}${req.url}`, // URL completo per Angular routing
      document: template, // HTML template (vedi sotto)
      inlineCriticalCss: true, // Inline CSS critico nel 
    })
    .then((html) => {
      // HTML renderizzato con successo
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.send(html);
    })
    .catch((err) => {
      // Errore durante rendering
      console.error('SSR render error:', err);
      res.status(500).send('Server render error');
    });
});

// === SERVER START ===

const port = process.env['PORT'] || 4000;

app.listen(port, () => {
  console.log(`Angular SSR server running at http://localhost:${port}`);
});

Dettagli importanti:

Step 4: Differenza tra server.ts locale vs production

Locale (development):

npm run serve:ssr
// Avvia Express su :4000
// Legge dist/ (aggiorna su rebuild con --watch)
// Verboso logging
// Nessun caching header

Production (EC2/Heroku):

node dist/portfolio/server/main.js
// Avvia Express su :4000 (o PORT env var)
// Serve da /opt/portfolio/dist/ (path assoluto)
// Minimal logging (solo errori)
// Cache header aggressivo (1 anno asset immutable)
// Resilience4j circuit breaker se chiama API backend

Step 5: Build SSR locale

Una volta configurato:

# Build browser + server bundle
npm run build

# Vedi l'output:
# dist/portfolio/browser/     ← CSS, JS, images (client-side)
# dist/portfolio/server/      ← JavaScript server bundle

# Avvia SSR server locale
npm run serve:ssr

# Visita http://localhost:4000
# Ispezione: view-source del HTML — vedrai contenuto renderizzato (non )

Step 6: Porte e comandi npm

Dopo `ng add @angular/ssr`, il tuo `package.json` avrà questi script:

"scripts": {
  "ng": "ng",
  "start": "ng serve",                       // CSR localhost:4200
  "build": "ng build",                       // Build browser + server SSR
  "serve:ssr": "node dist/portfolio/server/main.js", // SSR localhost:4000
  "prerender": "ng run portfolio:prerender"  // SSG: genera HTML statico
}

Quando usare ciascun comando:

Step 7: Zone.js vs zoneless

Nel 2026, Angular 21 supporta il flag zoneless (experimental ma quasi stable):

Con Zone.js (default attualmente):

// src/main.ts
import 'zone.js';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent);

Zone.js è una libreria che intercetta tutti gli async (setTimeout, Promise, fetch) per triggerare change detection Angular. Pro: automatic, Contro: 90KB di overhead, complexity.

Zoneless (experimental, future default):

// Aggiungi a angular.json
"projects": {
  "portfolio": {
    "architect": {
      "build": {
        "options": {
          "aot": true,
          "optimization": true,
          "sourceMap": false,
          "ngZone": "zone.js"  // ← cambia a "noop"
        }
      }
    }
  }
}

Zoneless usa i Signal/onPush change detection per essere reattivo senza Zone.js. Più piccolo, più predicibile. Nel 2026 userai zoneless, ma nel corso facciamo esempi con Zone.js (default still).

Troubleshooting: errori comuni SSR

Errore 1: "window is not defined"

Problema: Il tuo componente usa `window.localStorage` o `window.location`. In server Node.js, `window` non esiste.

Soluzione: Usa `isPlatformBrowser()` per eseguire codice solo client:

import { Component, OnInit, inject } from '@angular/core';
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({
  selector: 'app-analytics',
  standalone: true,
  template: `
Analytics tracker
`, }) export class AnalyticsComponent implements OnInit { private platformId = inject(PLATFORM_ID); ngOnInit() { // Esegui solo nel browser if (isPlatformBrowser(this.platformId)) { const trackingId = window.localStorage.getItem('tracking_id'); console.log('Tracking ID:', trackingId); } } }

Errore 2: "document is not defined"

Problema: Usi `document.querySelector()` in un service o componente eseguito server-side.

Soluzione: Stessa cosa — wrappa in `isPlatformBrowser()`:

export class DomService {
  private platformId = inject(PLATFORM_ID);

  getElement(selector: string) {
    if (isPlatformBrowser(this.platformId)) {
      return document.querySelector(selector);
    }
    return null;
  }
}

Errore 3: "Hydration mismatch"

Problema: HTML renderizzato server ≠ HTML generato client dopo idratazione. Angular console log: "HYDRATION_MISMATCH_DETECTED".

Causa comune: Visualizzi dati diversi server vs client (ad es., timezone locale, flag browser).

Soluzione: Abilita deferred hydration (modulo 03 parleremo di questo).

Errore 4: "Timeout during SSR"

Problema: Una richiesta HTTP nel componente non finisce in tempo, server aspetta eternità.

Soluzione: Aggiungi timeout alla HttpClient:

import { timeout } from 'rxjs/operators';

export class DataService {
  constructor(private http: HttpClient) {}

  getData() {
    return this.http.get('/api/data').pipe(
      timeout(5000) // 5 secondi timeout
    );
  }
}

Lab esercizio: Convertire una piccola app Angular in SSR

Obbiettivo: Prendi un progetto Angular 21 piccolo (3-5 componenti) e aggiungici SSR.

Passi:

  1. ng new demo-ssr --routing (crea una nuova app)
  2. ng add @angular/ssr (aggiungi SSR)
  3. Crea 3 componenti semplici (home, about, articles list)
  4. npm run build (compila SSR)
  5. npm run serve:ssr (testa su :4000)
  6. Usa View Source — verifica che vedi HTML renderizzato (non solo ``)
  7. Confronta CSR (`npm start` on :4200) vs SSR (`:4000`) — noti differenza LCP?

Code repo (coming soon): github.com/fedcal01/angular-ssr-masterclass-labs/tree/main/module-02

Recap: cosa sai adesso

Prossimi passi

Nel modulo 03 parleremo di standalone components e signals server-side. Vedrai come scrivere componenti che funzionano bene sia server che client, usando pattern moderni Angular 21 (signal inputs, effects condizionali, deferred rendering).

→ Vai al modulo 03: Standalone components + signals server-side


Domande? Chiedi nei commenti sotto o su GitHub Discussions.