Special thanks to Rob Wormald for sharing an example that clarified how server side rendering works in Angular, for answering a few questions, and for reviewing this piece. Equally grateful to my colleague Hans-Peter Grahsl for his help in polishing this text.

Server side prerendering, particularly for consumer-facing apps, brings back the perks of traditional websites without sacrificing what modern JavaScript frameworks offer. Not only does it improve loading speed, which can boost conversion rates, but it also enables social sharing with proper previews and can aid SEO, given that search engines have been crawling server-rendered pages for over twenty years. That said, engines like Google are steadily improving their ability to index JavaScript-dependent sites.

Angular has supported this capability since early on. In version 2, developers turned to the community-driven Angular Universal project. Since server side rendering is seen as a key feature for Google's SPA framework, the core team integrated a reworked version directly into Angular itself, starting with version 4.

Here, I outline how to take an existing Angular 4 application and equip it with server side rendering. The setup relies on a webpack configuration generated via the Angular CLI. The complete project is available for reference.

Server side rendering introduces added complexity to any project. For that reason, it makes sense to adopt it only when those benefits are actually needed.

Ejecting the Angular CLI

If you're working with the Angular CLI, ejection is the first step, as it gives you direct access to modify the build process via webpack:

ng eject

Before ejecting, be sure you understand the implications of doing so.

The CLI release used in this article omitted the Uglyfy-Plugin, which strips out code that webpack flags as dead. Moreover, it configured the AotPlugin with skipCodeGeneration set to true, which disables AOT. To activate these features, I adjusted the generated webpack.config.js in the following manner:

[...]
"plugins": [
    [...],
    new AotPlugin({
      "mainPath": "src/main.ts",
      "hostReplacementPaths": {
        "environments\\environment.ts": "environments\\environment.ts"
      },
      "exclude": [],
      "tsConfigPath": "tsconfig.json",

      // Set flag to false to allow AOT
      "skipCodeGeneration": false
    }),

    // Add UgilyJsPlugin
    new webpack.optimize.UglifyJsPlugin()
]
[...]

While AOT isn't a prerequisite for server side rendering, pairing them is advantageous because both help cut down the time it takes for the app to load.

Necessary Packages

For the server-side portion, this approach relies on Node.js combined with Express. Consequently, I installed the express package, the corresponding type definitions (@types/express), and @angular/platform-server:

npm i @angular/platform-server@4.0.0-rc.2 --save
npm i express --save
npm i @types/express --save-dev

It's important to align the version of @angular/platform-server with the other Angular packages in use. In this instance, the version was 4.0.0-rc.2.

Creating a Root Module for Server Side Rendering

In order to leverage server side rendering, a dedicated root module that incorporates the ServerModule is required. Following Rob Wormald's approach, I brought in the existing browser root module as well. This lets me stick to the DRY principle without having to restructure the current module layout:

// app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';

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

The client-side root module, meanwhile, brings in the BrowserModule using its static withServerTransition method. This method requires an id to be provided for the specific application:

// app.module.ts

@NgModule({
    imports: [
        BrowserModule.withServerTransition({
            appId: 'demo-app'
        }),
        HttpModule,
        FormsModule,
        [...]
    ],
    [...]
})
export class AppModule {
}

AOT for the server side

Since the CLI and its AotPlugin didn't offer AOT support for server code at the time, the demonstration makes direct use of the Angular Compiler. To set this up, I made a duplicate of tsconfig.json called tsconfig.server.json. Within it, the following angularCompilerOptions are specified:

"compilerOptions": {
    [...]
},
[...]
"angularCompilerOptions": {
  "genDir": "src/aot",
  "entryModule": "./src/app.server.module#AppServerModule"
}

The ngc:server script, located in package.json, triggers the Angular Compiler:

[...]
"scripts": {
    [...]
    "ngc:server": "ngc -p tsconfig.server.json"
}
[...]

Running this script (npm run ngc:server) causes the compiler to generate the usual supplementary TypeScript files. With the AppServerModuleNgFactory produced for the AppServerModule, the main.server.ts file initiates a node process that handles prerendering:

// main.server.ts
// Modified version of equivalent file in 
// https://github.com/robwormald/ng-universal-demo/

import 'zone.js/dist/zone-node';
import { platformServer, renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { AppServerModule } from './app/app.server.module';
import { AppServerModuleNgFactory } from './aot/src/app/app.server.module.ngfactory';
import * as express from 'express';
import {ngExpressEngine} from './express-engine';

enableProdMode();

const app = express();

app.engine('html', ngExpressEngine({
    baseUrl: 'http://localhost:8000',
    bootstrap: [AppServerModuleNgFactory],
}));

app.set('view engine', 'html');
app.set('views', '.')

app.get('/', (req, res) => {
    res.render('index', {req});
});

app.get('/home*', (req, res) => {
    res.render('index', {req});
});

app.get('/flight-booking*', (req, res) => {
    res.render('index', {req});
});

app.get('/passenger*', (req, res) => {
    res.render('index', {req});
});

app.get('/history*', (req, res) => {
    res.render('index', {req});
});

app.use(express.static('.'));

app.listen(8000,() => {
    console.log('listening...');
});

Keep in mind that these server-side routes do more than just prerender components; they also serve static assets, including the client-side bundles.

This file draws inspiration from a similar one in Rob's example. Additionally, I adopted his express engine, which is responsible for triggering prerender:

// express-engine.ts
// Taken from https://github.com/robwormald/ng-universal-demo/

import { renderModuleFactory } from '@angular/platform-server';

import * as fs from 'fs';
import * as path from 'path';

const templateCache  = {};

export function ngExpressEngine(setupOptions){

    return function(filePath, options, callback){
        if(!templateCache[filePath]){
            let file = fs.readFileSync(filePath);
            templateCache[filePath] = file.toString();
        }
        renderModuleFactory(setupOptions.bootstrap[0], {
            document: templateCache[filePath],
            url: options.req.url
        })
        .then(string => {
            callback(null, string);
        });
    }
}

Webpack configuration for server side rendering

For building the server version, the example duplicates the current webpack.config.js as webpack.server.js. In practice, it's advisable to avoid such duplication of configuration, but for this demonstration it works fine.

This setup specifies node as the target to create a server bundle, with just a single entry point:

  // main.server.ts

  [...]
  target: 'node',
  [...]
  "entry": {
    "main": [
      "./src/main.server.ts"
    ]
  },

Given the use of a single bundle, I took out both instances of the CommonsChunkPlugin. To make tinkering easier, the NoEmitOnErrorsPlugin was also removed.

To keep webpack from overwriting the client output, the configuration adopts the xyz.server.bundle.js naming convention for its generated files:

  "output": {
    "path": path.join(process.cwd(), "dist"),
    "filename": "[name].server.bundle.js",
    "chunkFilename": "[id].server.chunk.js"
  },

To circumvent certain complications, the solution employs the AotPlugin alongside the direct Angular Compiler usage:

new AotPlugin({
  "entryModule": __dirname + "/src/app/app.server.module.ts#AppServerModule",
  "hostReplacementPaths": {
    "environments\\environment.ts": "environments\\environment.ts"
  },
  "exclude": [],
  "tsConfigPath": "./tsconfig.server.json",
  "skipCodeGeneration": false
}),

Build scripts

The example defines several npm scripts in package.json to manage the build, which involves both the Angular Compiler and webpack:

"scripts": {
    [...]
    "build": "npm run build:client",
    "build:client": "webpack",
    "build:server": "ngc -p tsconfig.server.json && webpack --progress --config webpack.server.config.js",
    "build:all": "npm run build:client && npm run build:server",
    [...]
}

Once these scripts are in place, a single call to npm run build:all compiles the app for both the client and the server.

Starting

After the build completes, you can move into the dist directory and launch the server.

cd dist
node main.server.bundle.js

The app will then be reachable at http://localhost:8000. The server takes care of prerendering the view that's requested. To see this in action, disable JavaScript temporarily and try using the menu to navigate; note that forms won't function without JS. Once the client bundle loads, Angular hydrates the page and takes over interactivity.

Further Thoughts

There are two additional topics that came up in my discussion with Rob Wormald. The first concerns passing the state from the server to the client. This is especially handy when the server pulls data from a Web API, as transferring that state avoids the client from making the same requests right after startup. Rob recommended using something akin to a Redux Store (see ngrx/Store) which can easily be serialized and embedded in the page as a JSON "data island". This alone is a topic that deserves its own detailed write-up.

The second part of our conversation touched on the "uncanny valley"—the gap between when the server-rendered page arrives and when the client code becomes active. The moment the latter kicks in, the application state is reinitialized, which also wipes out any information a user might have typed into forms. Angular 4's implementation doesn't address this gap, and we agreed that solving it is quite a challenge. It's probably wise to handle this with scenario-specific strategies, whether that's designing the app to minimize such pitfalls or using existing custom solutions that match the particular use case. An insightful idea on this, along with some compelling performance data, is available at that link.