Skip to main content

Lab 25: Server-Side Rendering (SSR)

πŸ“– Resources​

πŸš€ 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​

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 renders
SSR flow: Server renders HTML β†’ browser displays instantly β†’ Angular hydrates

πŸ“ Instructions​

Step 1: Add SSR to the Project​

ng add @angular/ssr

This schematic does all the heavy lifting. It:

  • Installs @angular/ssr and express
  • 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.json with "server" and "ssr" build options
  • Adds a serve:ssr script to package.json

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);
Two configs, merged

Angular maintains two separate configs: app.config.ts for the browser and app.config.server.ts for the server. The server config uses mergeApplicationConfig to inherit all browser providers, then adds provideServerRendering() on top.

This separation matters: browser-only providers like provideStoreDevtools() (NgRx DevTools) stay in app.config.ts only β€” they must never appear in app.config.server.ts, where there is no browser environment to attach to.

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​

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();
});
}
}
HttpClient on the server

HTTP requests made with HttpClient during SSR are automatically transferred to the browser via Angular's state transfer mechanism β€” the browser does not re-fetch them. This works automatically when you use provideHttpClient(withFetch()) in your app config. No extra code needed for standard API calls.

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​

ng build

This generates two output folders inside dist/<project-name>/:

  • browser/ β€” static assets served to the client
  • server/ β€” Node.js server bundle

Tip: The exact dist path matches the outputPath in angular.json. Verify with ls dist/.

Run the SSR server:

node dist/album-wholesale-v17/server/server.mjs

Open 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​

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​

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.