Lab 25: Server-Side Rendering (SSR)
📖 Resources
Section titled “📖 Resources”🚀 Starter Code
Section titled “🚀 Starter Code”album-wholesale-v17-signal-pre-jest
In this lab you will enable Server-Side Rendering in the album wholesale app. SSR pre-renders pages on the server, improving initial load time and SEO. You will handle browser-only APIs safely, transfer state to avoid double fetching, and run the SSR server locally.
💡 How SSR Works
Section titled “💡 How SSR Works”With Client-Side Rendering (CSR), the server sends a blank HTML shell — the browser receives <app-root></app-root> empty, then downloads and executes JavaScript to build the page.
With Server-Side Rendering (SSR), the server renders the full HTML first. The browser receives a complete page, which is faster to display and indexable by search engines. Angular then hydrates the page — it attaches event listeners and takes over without re-rendering.
CSR flow: Server → blank HTML → browser downloads JS → browser rendersSSR flow: Server renders HTML → browser displays instantly → Angular hydrates📝 Instructions
Section titled “📝 Instructions”Step 1: Add SSR to the Project
Section titled “Step 1: Add SSR to the Project”ng add @angular/ssrThis schematic does all the heavy lifting. It:
- Installs
@angular/ssrandexpress - Creates
src/main.server.ts— server bootstrap entry point - Creates
server.ts— the Express HTTP server at project root - Creates
src/app/app.config.server.ts— server-specific providers - Updates
angular.jsonwith"server"and"ssr"build options - Adds a
serve:ssrscript topackage.json
Step 2: Review the Server Config
Section titled “Step 2: Review the Server Config”Inspect the generated app.config.server.ts:
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';import { provideServerRendering } from '@angular/platform-server';import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = { providers: [provideServerRendering()],};
export const config = mergeApplicationConfig(appConfig, serverConfig);Step 3: Guard Browser-Only Code
Section titled “Step 3: Guard Browser-Only Code”The server has no access to window, localStorage, or document. Guard browser-only code with isPlatformBrowser:
import { PLATFORM_ID, inject } from '@angular/core';import { isPlatformBrowser } from '@angular/common';
export class AlbumDetailComponent { private platformId = inject(PLATFORM_ID);
ngOnInit() { if (isPlatformBrowser(this.platformId)) { // Safe to access window, localStorage, etc. const saved = localStorage.getItem('recentAlbums'); } }}Step 4: Use afterNextRender for DOM Access
Section titled “Step 4: Use afterNextRender for DOM Access”Prefer afterNextRender (Angular 16+) over lifecycle hooks for browser-only side effects:
import { afterNextRender } from '@angular/core';
export class AlbumPlayerComponent { constructor() { afterNextRender(() => { // Runs only in the browser, after first render this.initAudioPlayer(); }); }}Step 5: Transfer State to Avoid Double Fetching
Section titled “Step 5: Transfer State to Avoid Double Fetching”Without transfer state, Angular re-fetches data in the browser even though the server already fetched it. Fix this with TransferState:
import { TransferState, makeStateKey } from '@angular/core';import { inject } from '@angular/core';
const ALBUMS_KEY = makeStateKey<Album[]>('albums');
export class AlbumService { private transferState = inject(TransferState); private http = inject(HttpClient);
getAlbums(): Observable<Album[]> { const cached = this.transferState.get(ALBUMS_KEY, null); if (cached) { this.transferState.remove(ALBUMS_KEY); return of(cached); } return this.http.get<Album[]>('/api/albums').pipe( tap(albums => this.transferState.set(ALBUMS_KEY, albums)) ); }}Also enable hydration in app.config.ts:
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient(), provideClientHydration(), ],};Step 6: Build and Run the SSR Server
Section titled “Step 6: Build and Run the SSR Server”ng buildThis generates two output folders inside dist/<project-name>/:
browser/— static assets served to the clientserver/— Node.js server bundle
Tip: The exact dist path matches the
outputPathinangular.json. Verify withls dist/.
Run the SSR server:
node dist/album-wholesale-v17/server/server.mjsOpen http://localhost:4000 and check:
- View source (Ctrl+U) — page HTML should already contain album content
- Network tab — no extra API calls after hydration
Step 7: Verify SEO Improvement
Section titled “Step 7: Verify SEO Improvement”Open the page source and confirm:
<title>tag contains the album name- Meta description is present
- Album list HTML is visible in the source (not just
<app-root></app-root>)
This confirms the page is rendered on the server before being sent to the browser.
✅ Classroom Validation
Section titled “✅ Classroom Validation”The clearest demo of SSR working: open DevTools → Network → refresh the page and click the initial HTML document request. Look at the Response tab.
- CSR: you see
<app-root></app-root>— empty, no content - SSR: you see full album list HTML inside
<app-root>— rendered on the server
No plugins needed. This single comparison makes the value of SSR immediately tangible.

