Setting Up Angular Universal with Angular 14
First, make sure your Angular 14 project includes Angular Universal support. Use the Angular CLI to add it:
ng add @nguniversal/express-engine
This command sets up everything you need, including a server module, Express server, and TypeScript configuration.
server.ts - Basic Express Setup
import 'zone.js/node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
const app = express();
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
app.set('view engine', 'html');
app.set('views', join(__dirname, 'browser'));
app.get('*', (req, res) => {
res.render('index', { req });
});
app.listen(4000, () => {
console.log(`Node server listening on http://localhost:4000`);
});
Under the Hood: SSR Lifecycle Explained
When a request comes in, the Universal engine processes it using the following steps:
ngExpressEngine()wraps Angular’sCommonEngine.- Each request calls
CommonEngine.render(). render()internally calls Angular’srenderModule()from@angular/platform-server.
SSR Flow in Angular Universal
renderModule(AppServerModule, {
document: indexHtml,
url: req.url
}).then(html => res.send(html));
- A new
PlatformRefis created. - Angular bootstraps the app module.
- It waits for the app to become stable (i.e. all async tasks complete).
- The DOM (powered by the Domino library) is serialized into HTML and sent back.
Application Stability Before Rendering
Angular checks when the app is stable using ApplicationRef.isStable. Only after all pending async tasks are done does it continue rendering:
appRef.isStable
.pipe(first(isStable => isStable))
.subscribe(() => serializeDocument());
This guarantees that HTTP-loaded data is rendered and visible in the HTML response.
Cleanup: Destroying the App
After rendering is complete:
PlatformRef.destroy()is called.- App module, root component, and all services are torn down.
- All
ngOnDestroy()lifecycle hooks are invoked.
This frees memory and ensures no SSR state leaks into other requests.
Gotchas and Best Practices
Pending Async Tasks → Memory Leaks
If a request never completes (e.g. hanging HTTP call), the app never stabilizes. This prevents rendering and causes memory leaks.
Shared Global State → Race Conditions
Avoid using global variables or static state. SSR renders apps in parallel, and shared mutable state can lead to race conditions.
Missing Unsubscribe → Memory Waste
Make sure to clean up all RxJS subscriptions:
ngOnDestroy() {
this.subscription?.unsubscribe();
}
Even services should handle teardown logic to avoid server memory bloat.
Summary
- Use
@nguniversal/express-enginefor Angular 14 SSR. - Rendering uses
renderModule()from@angular/platform-server. - Angular waits until app stability before HTML serialization.
- Always clean up subscriptions and avoid shared state.
SSR in Angular 14 with Universal gives you faster first-paint times and better SEO. But it requires precise control over lifecycle management and asynchronous logic.
Master these fundamentals, and your server-rendered Angular apps will scale beautifully.
