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
Add Angular SSR to the app (skip if already server-rendered).
ng add @angular/ssrThis scaffolds
main.server.ts,server.ts, and the server build target.Install
@rx-angular/isr.npm install @rx-angular/isrRegister the ISR providers in your standalone server config (
main.server.ts). AddprovideISR()and register theisrHttpInterceptorswithprovideHttpClient(withInterceptors(...)).main.server.tsimport { 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);Instantiate
ISRHandlerinserver.tsand wire it into the request pipeline. Pass the render engine, eithercommonEngineorangularAppEngine. This recipe usesCommonEngine; theAngularNodeAppEnginevariant is shown below.server.tsimport { 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 AngularNodeAppEngineinsteadOn the newer
@angular/ssrapplication builder output, passangularAppEngineinstead ofcommonEngine. Both areISRHandlerConfigfields; 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,
});Mark the routes you want cached with a
revalidatevalue in the routedata. Routes withoutrevalidateare passed through un-cached.app.routes.tsimport { Routes } from '@angular/router';
import { RouteISRConfig } from '@rx-angular/isr/models';
export const routes: Routes = [
{
path: 'home',
component: HomeComponent,
data: { revalidate: 100 } as RouteISRConfig,
},
];revalidateis the number of seconds after which the cached page is regenerated on the next hit. SeeRouteISRConfigfor thenull/0/Nvalue semantics.Run it in development. On Angular v17.1+ (v21 here),
ng serveruns yourserver.ts, so ISR works under the normal dev-server:ng serveOn an older builder that does not execute
server.tsin dev, build and serve the server bundle directly instead:ng build --watch --configuration development, thennode 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/isrAPI:ISRHandler,provideISR,isrHttpInterceptors. - Reference:
ISRHandlerConfig: all 18 config fields and theRouteISRConfigroute data. - Concept: How ISR works (E8): the route-data capture → cache-decision mechanism this recipe assumes.