Skip to main content

Angular Universal Setup

Introduction

Angular Universal is the official library for implementing Server-Side Rendering (SSR) in Angular applications. While traditional Angular applications render entirely in the browser (client-side), Angular Universal allows your app to render on the server first, sending a complete HTML page to the client. This approach offers several benefits:

  • Improved SEO: Search engines can crawl your content more effectively
  • Faster perceived loading: Users see content immediately instead of waiting for JavaScript to load
  • Better performance on mobile/low-powered devices: Less work is done on the client
  • Enhanced social media sharing: Proper previews when sharing links on social platforms

In this guide, we'll walk through the process of setting up Angular Universal for an existing Angular application step by step.

Prerequisites

Before we begin, make sure you have:

  • An existing Angular application (Angular 9+)
  • Node.js (v12+) and npm installed
  • Basic understanding of Angular concepts

Setting Up Angular Universal

Step 1: Add Universal to Your Project

Angular provides a schematic that automates the setup process for Universal. Run the following command in your project directory:

bash
ng add @nguniversal/express-engine

This command performs several important tasks:

  • Adds the necessary dependencies to your package.json
  • Creates a server-side application module (app.server.module.ts)
  • Creates a server entry point (server.ts)
  • Updates your angular.json file with server configuration
  • Modifies your app.module.ts to make it compatible with SSR

Step 2: Understanding the Generated Files

Let's examine the key files that were generated:

app.server.module.ts

typescript
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

This module imports your main AppModule and adds the necessary server-specific functionality.

server.ts

typescript
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/your-app-name/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
? 'index.original.html'
: 'index';

// Our Universal engine
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));

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

// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));

// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

return server;
}

function run(): void {
const port = process.env.PORT || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

export * from './src/main.server';

This file sets up an Express server that uses the Angular Universal engine to render your application.

Step 3: Configure the Application for Universal

Your application might need some adjustments to work properly with SSR. Here are some common modifications:

Update Client Code for SSR Compatibility

In your app.module.ts, import BrowserModule with withServerTransition:

typescript
import { BrowserModule, BrowserTransitionOptions } from '@angular/platform-browser';

@NgModule({
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
// other imports
],
// ...
})
export class AppModule { }

Step 4: Handle Browser-Specific APIs

One challenge with SSR is that browser APIs aren't available during server rendering. You need to handle this by:

  1. Using Angular's isPlatformBrowser to check the execution environment
  2. Using guards for browser-only code

Create a service to handle platform detection:

typescript
// platform.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({
providedIn: 'root'
})
export class PlatformService {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
}

Use this service in your components:

typescript
// example.component.ts
import { Component, OnInit } from '@angular/core';
import { PlatformService } from './platform.service';

@Component({
selector: 'app-example',
template: `<div>Example Component</div>`
})
export class ExampleComponent implements OnInit {
constructor(private platformService: PlatformService) {}

ngOnInit() {
if (this.platformService.isBrowser()) {
// Execute browser-only code here
window.localStorage.setItem('visited', 'true');
}
}
}

Step 5: Running Your Universal Application

The ng add command adds new npm scripts to your package.json:

json
"scripts": {
"dev:ssr": "ng run your-app-name:serve-ssr",
"serve:ssr": "node dist/your-app-name/server/main.js",
"build:ssr": "ng build && ng run your-app-name:server",
"prerender": "ng run your-app-name:prerender"
}

To test your SSR application locally:

  1. Build the application: npm run build:ssr
  2. Start the server: npm run serve:ssr
  3. Open your browser to http://localhost:4000

Real-World Example: News Article Website

Let's imagine we're building a news website where SEO is crucial. Here's how we might structure a simple article component:

typescript
// article.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Meta, Title } from '@angular/platform-browser';
import { ArticleService } from './article.service';

@Component({
selector: 'app-article',
template: `
<article *ngIf="article">
<h1>{{article.title}}</h1>
<div class="metadata">
<span>By {{article.author}}</span>
<span>{{article.date | date:'longDate'}}</span>
</div>
<div class="content" [innerHTML]="article.content"></div>
</article>
`
})
export class ArticleComponent implements OnInit {
article: any;

constructor(
private route: ActivatedRoute,
private articleService: ArticleService,
private title: Title,
private meta: Meta
) {}

ngOnInit() {
const articleId = this.route.snapshot.paramMap.get('id');
this.articleService.getArticle(articleId).subscribe(article => {
this.article = article;

// Set meta tags for SEO
this.title.setTitle(article.title);
this.meta.addTags([
{ name: 'description', content: article.summary },
{ property: 'og:title', content: article.title },
{ property: 'og:description', content: article.summary },
{ property: 'og:image', content: article.featureImage },
{ property: 'og:url', content: `https://example.com/article/${articleId}` },
{ name: 'twitter:card', content: 'summary_large_image' }
]);
});
}
}

With Angular Universal, this component will render on the server with all meta tags properly set, ensuring search engines and social media platforms can see your content.

Common Issues and Solutions

1. Window is not defined

Problem: Server doesn't have window object.

Solution: Use platform detection:

typescript
import { PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
// Safe to use window here
}
}

2. LocalStorage is not available

Problem: Server doesn't have access to browser storage.

Solution: Create an abstract storage service:

typescript
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({
providedIn: 'root'
})
export class StorageService {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

setItem(key: string, value: string): void {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem(key, value);
}
}

getItem(key: string): string | null {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem(key);
}
return null;
}
}

3. Third-party Libraries Using Browser APIs

Problem: Libraries expecting browser APIs fail on server.

Solution: Dynamically import libraries on the client side:

typescript
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({
selector: 'app-chart',
template: '<div #chartContainer></div>'
})
export class ChartComponent implements OnInit {
private chartLibrary: any;

constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

async ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Dynamically import chart library only in browser
this.chartLibrary = await import('chart.js');
this.initChart();
}
}

initChart() {
// Initialize chart using the library
}
}

Performance Optimization Tips

  1. State Transfer: Use TransferState to avoid duplicate data fetching:
typescript
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';

const DATA_KEY = makeStateKey<any>('myData');

export class DataService {
constructor(
private http: HttpClient,
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object
) {}

getData() {
// Check if we have data from the server
const cachedData = this.transferState.get(DATA_KEY, null);

if (cachedData) {
return of(cachedData);
}

return this.http.get('/api/data').pipe(
tap(data => {
if (isPlatformServer(this.platformId)) {
// Store data in transfer state if on server
this.transferState.set(DATA_KEY, data);
}
})
);
}
}
  1. Lazy Loading: Continue to use Angular's lazy loading for modules:
typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule)
}
];
  1. Prerendering: For mostly static content, consider prerendering:
bash
npm run prerender

Summary

In this tutorial, we've covered:

  1. Adding Angular Universal to an existing Angular application
  2. Understanding the structure of server-side rendering setup
  3. Handling browser-specific APIs in an SSR context
  4. Implementing SEO improvements with meta tags
  5. Managing common SSR issues
  6. Optimizing performance with state transfer and prerendering

By implementing Angular Universal, your application will benefit from improved SEO, faster initial load times, and better user experience on slow connections or devices.

Additional Resources

Exercises

  1. Add Angular Universal to an existing project and identify any browser API issues
  2. Create a service that safely handles localStorage in an SSR environment
  3. Implement meta tags for SEO on your application's main routes
  4. Use TransferState to prevent duplicate HTTP requests
  5. Benchmark your application's load time before and after implementing SSR


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)