Skip to main content

How to set up ISR in an SSR app

Goal

Wire @rx-angular/isr into an existing Angular SSR application so that routes marked with revalidate are cached and served from cache. This recipe assumes you already understand how ISR works (route-data capture → cache decision at render-finish); if you don't, read the How ISR works concept first. This page is the task only; every symbol used here is documented in the ISR API reference and ISRHandlerConfig.

Steps

  1. Add Angular SSR to the app (skip if already server-rendered).

    ng add @angular/ssr

    This scaffolds main.server.ts, server.ts, and the server build target.

  2. Install @rx-angular/isr.

    npm install @rx-angular/isr
  3. Register the ISR providers in your standalone server config (main.server.ts). Add provideISR() and register the isrHttpInterceptors with provideHttpClient(withInterceptors(...)).

    main.server.ts
    import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
    import { provideServerRendering } from '@angular/ssr';
    import { provideHttpClient, withInterceptors } from '@angular/common/http';
    import { provideISR, isrHttpInterceptors } from '@rx-angular/isr/server';

    import { appConfig } from './app/app.config';

    const serverConfig: ApplicationConfig = {
    providers: [
    provideServerRendering(),
    provideISR(), // 👈 register the ISR providers
    provideHttpClient(withInterceptors(isrHttpInterceptors)),
    ],
    };

    export const config = mergeApplicationConfig(appConfig, serverConfig);
  4. Instantiate ISRHandler in server.ts and wire it into the request pipeline. Pass the render engine, either commonEngine or angularAppEngine. This recipe uses CommonEngine; the AngularNodeAppEngine variant is shown below.

    server.ts
    import { CommonEngine } from '@angular/ssr/node';
    import express from 'express';
    import { fileURLToPath } from 'node:url';
    import { dirname, join, resolve } from 'node:path';
    import bootstrap from './src/main.server';

    import { ISRHandler } from '@rx-angular/isr/server';

    export function app(): express.Express {
    const server = express();
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');
    const indexHtml = join(serverDistFolder, 'index.server.html');

    const commonEngine = new CommonEngine();

    // Query params allowed in the cache key. `undefined` = all, `[]` = none.
    const allowedQueryParams = ['page'];

    const isr = new ISRHandler({
    indexHtml,
    invalidateSecretToken: process.env['REVALIDATE_SECRET_TOKEN'] ?? 'MY_TOKEN',
    enableLogging: true,
    serverDistFolder,
    browserDistFolder,
    bootstrap,
    commonEngine,
    allowedQueryParams,
    });

    server.use(express.json());
    server.post('/api/invalidate', async (req, res) => await isr.invalidate(req, res));

    server.set('view engine', 'html');
    server.set('views', browserDistFolder);

    server.get('*.*', express.static(browserDistFolder, { maxAge: '1y' }));

    server.get(
    '*',
    // Serve the page if it exists in cache…
    async (req, res, next) => await isr.serveFromCache(req, res, next),
    // …otherwise render it and add it to the cache when needed.
    async (req, res, next) => await isr.render(req, res, next),
    );

    return server;
    }
    Using AngularNodeAppEngine instead

    On the newer @angular/ssr application builder output, pass angularAppEngine instead of commonEngine. Both are ISRHandlerConfig fields; supply exactly one.

    server.ts (excerpt)
    import { AngularNodeAppEngine } from '@angular/ssr/node';

    const angularAppEngine = new AngularNodeAppEngine();

    const isr = new ISRHandler({
    indexHtml,
    invalidateSecretToken: process.env['REVALIDATE_SECRET_TOKEN'] ?? 'MY_TOKEN',
    serverDistFolder,
    browserDistFolder,
    bootstrap,
    angularAppEngine,
    });
  5. Mark the routes you want cached with a revalidate value in the route data. Routes without revalidate are passed through un-cached.

    app.routes.ts
    import { Routes } from '@angular/router';
    import { RouteISRConfig } from '@rx-angular/isr/models';

    export const routes: Routes = [
    {
    path: 'home',
    component: HomeComponent,
    data: { revalidate: 100 } as RouteISRConfig,
    },
    ];

    revalidate is the number of seconds after which the cached page is regenerated on the next hit. See RouteISRConfig for the null / 0 / N value semantics.

  6. Run it in development. On Angular v17.1+ (v21 here), ng serve runs your server.ts, so ISR works under the normal dev-server:

    ng serve

    On an older builder that does not execute server.ts in dev, build and serve the server bundle directly instead: ng build --watch --configuration development, then node dist/your-app/server/server.mjs.

Result

Request a revalidate-enabled route twice. The first request server-renders and caches the page; the second request is served from the cache. With enableLogging: true the ISR logs show a cache hit, and the response returns without a fresh render.

See also

  • Reference: @rx-angular/isr API: ISRHandler, provideISR, isrHttpInterceptors.
  • Reference: ISRHandlerConfig: all 18 config fields and the RouteISRConfig route data.
  • Concept: How ISR works (E8): the route-data capture → cache-decision mechanism this recipe assumes.