How To Use Angular Universal for Server Side Rendering



Single-page Applications (SPAs) consist of a single HTML document that is served initially to a client. Any new views that are required in the app are generated solely on the client via JavaScript. The request-response cycle still happens, but this is usually only to RESTful APIs for data; or to get static resources, like images.

There are a lot of benefits to writing applications this way. However, there are some things that you lose, such as the ability for web crawlers to traverse your app and slower performance while the app is loading, which can take a significant amount of time. In comes Server-Side Rendering (SSR) to bridge the gap.

In this article, you will learn how to use Angular Universal for Server-Side Rendering.


To follow along with this tutorial, you will need:

This tutorial was verified with Node v16.4.2, npm v7.19.1, @angular/core v12.1.1, and @nguniversal/express-engine v12.1.0.

Step 1 — Getting Started with Angular Universal

An Angular application is a Single-page App - it runs in a client’s browser. Angular Universal, however, lets you also run your Angular app on the server. This enables you to serve static HTML to the client. With Angular Universal, the server will pre-render pages and show your users something, while the client-side app loads in the background. Then, once everything is ready client-side, it will seamlessly switch from showing the server-rendered pages to the client-side app. Your users shouldn’t notice a difference, beyond the fact that instead of waiting for your “Loading” spinner to finish, they at least can have some content to keep them engaged until they can start using the fully-featured client-side application.

SSR with Angular Universal requires changes in both the client application and the server stack to work. For this article, we’ll assume this is a brand new Angular application, barely created with the Angular CLI, at version >= 7. Just about any server technology can run a Universal app, but it has to be able to call a special function, renderModuleFactory(), provided by Angular Universal, which is itself a Node package; so serving this Angular application from a Node/Express server makes sense for this example.

We’ll be using a schematic to get us up and going quickly.

From your app directory open a terminal and run the following command:

  • ng add @nguniversal/express-engine

You’ll notice this schematic made several changes to your app, modifying some files and adding some files:

Updates angular.json

  • projects.{{project-name}}.architect.build.options.outputPath changes to "dist/browser"
  • a new projects.{{project-name}}.architect is added, called "server"

This lets the Angular CLI know about our server/Universal version of the Angular application.

Updates package.json

Besides a few new dependencies (to be expected), we also get a few new scripts:

  • "dev:ssr"
  • "build:ssr"
  • "serve:ssr"
  • "prerender"

Updates main.ts

This has been modified so that the browser version of the app won’t start bootstrapping until the Universal-rendered pages have been fully loaded.

Updates app.module.ts

Modified to execute the static method .withServerTransition on the imported BrowserModule. This tells the browser version of the application that the client will be transitioning in from the server version at some point.

Creates server.ts

This is the NodeJS Express server. Obviously, you don’t have to use this exact server setup as generated, although make note of the line:

server.engine('html', ngExpressEngine({ ... }))

ngExpressEngine is a wrapper around renderModuleFactory which makes Universal work. If you don’t use this exact server.ts file for your set-up, at least copy this part out and integrate it into yours.

Creates tsconfig.server.json

This tells the Angular compiler where to find the entry module for the Universal application.

Creates app.server.module.ts

This is the root module for the server version only. You can see it imports our AppModule, as well as the ServerModule from @angular/platform-server, and bootstraps the same AppComponent as AppModule. AppServerModule is the entry point of the Universal application.

Creates main.server.ts

This new file basically only exports the AppServerModule, which is the entry point of the Universal version of the application. We’ll revisit this soon.

Step 2 — Starting Your Universal App

From a command line, run the following command:

  • npm run build:ssr

And then run:

  • npm run serve:ssr

Assuming you didn’t hit any snags during the build process, open your browser to http://localhost:4000 (or whatever port is configured for you), and you should see your Universal app in action! It won’t look any different, but the first page should load much quicker than your regular Angular application. If your app is small and simple, this might be hard to notice.

You can try throttling the network speed by opening Chrome Dev Tools, and under the Network tab, finding the dropdown that says Online. Select Slow 3G to mimic a device on a slow network - you should still see good performance for your landing page, and any routed pages you go to.

Also, try viewing the page source (right-click on the page and select View Page Source). You’ll see all normal HTML in the <body> tag that matches what is displayed on your page - meaning, your application can be meaningfully scraped by a web crawler. Compare this with the page source of a non-Universal application, and all you’ll see in the <body> tag is <app-root> (or whatever you’ve called the selector for your bootstrapped AppComponent).


In this article, you learned how to use Angular Universal for Server-Side Rendering.

Since a Universal application runs on the server and not in a browser, there are a few things you need to watch out for in your application code:

  • Check your use of browser-specific objects, such as window, document, or location. These don’t exist on the server. You shouldn’t be using these anyway; try using an injectable Angular abstraction, such as Document or Location. As a last resort, if you do truly need them, wrap their usage in a conditional statement, so that they’ll only be used by Angular on the browser. You can do this by importing the functions isPlatformBrowser and isPlatformServer from @angular/common, injecting the PLATFORM_ID token into your component, and running the imported functions to see whether you’re on the server or the browser.
  • If you use ElementRef to get a handle on an HTML element, don’t use the nativeElement to manipulate attributes on the element. Instead, inject Renderer2 and use one of the methods there.
  • Browser event handling won’t work. Your app won’t respond to click events or other browser events when running on the server. However, any link generated from a routerLink will work for navigation.
  • Avoid the use of setTimeout, where possible.
  • Make all URLs for server requests absolute. Requests for data from relative URLs will fail when running from the server, even if the server can handle relative URLs.
  • Similarly, security around HTTP requests issued from a server isn’t the same as those issued from a browser. Server requests may have different security requirements and features. You’ll have to handle security on these requests yourself.

Continue learning about what Angular Universal provides in the official documentation.

Creative Commons License