Beginner’s guide to server-side rendering in Angular

Server-Side Rendering in Angular

Introduction

As the complexities of website and web experience keep increasing, we simultaneously need equally powerful tools, frameworks, and protocols to roll out the best functioning websites and applications.

Driven by the need to stand out from the competition and deliver ultimate user experiences, companies are becoming more innovative than ever. This is consistently pushing limits in terms of what developers can do technically and yet manage to offer captivating experiences.

At a time when there was a slight stagnation in the development, space arrived Angular with its insane functionalities and features.

Launched and maintained by Google, this open-source framework in TypeScript allows for the development of SPAs or Single Page Applications. This functionality requires tons of backend features that Angular delivers, specifically server-side rendering.

In this post, we will explore what server-side rendering is and how to create it on Angular.

Let’s get started.

Angular’s Server-side Rendering Module

Angular normally executes the application in the browser. It renders the pages in the DOM according to user interactions with the application. Rendering the angular application on the server means generating the static application pages and then loading them on the client-side. This makes the application load quicker, giving the users view of the application before it becomes interactive. Angular Universal is the technology that is responsible for rendering the application on the server.

When to use server-side rendering?

The three main reasons to create the universal version of the application are:

  1. Facilitate the web crawlers through search engine optimization (SEO).
  2. Improve performance on mobile and low powered devices.
  3. Show the first page quickly with a first contentful paint.

Facilitate web crawlers (SEO)

All social media websites rely on web crawlers to make the content of the application searchable on the web. These crawlers might not be able to navigate or index the highly interactive application like a normal human could do. So the angular universal will generate the static version of the application that makes the application easily searchable, linkable, etc.

Improve the performance on the mobile and low powered devices

The user experience on mobile and low-powered devices deteriorates as some of them do not support or execute JavaScript properly. In order to solve this, a server-side rendered or no JavaScript application is required to be developed.

Show the first page quickly

The loading of the first page faster is crucial for user engagement. The application whose pages run faster performs better. The application needs to load faster in order to engage the users more in the application.

With the help of Angular Universal, you can generate static pages that contain just the HTML and they are displayed even when the JavaScript is disabled. The browser events aren’t supported by these pages but the navigation is supported throughout the website using a router link.

So, the static version of the page is rendered to hold the user’s attention. At the same time, the whole Angular application will load behind it. So, the user gets to view the page instantly and gets the full interactive experience after the full application loads.

Steps to create the server side rendering in the existing angular application development project:

1. Create Browser and server modules

We need two modules which help angular cli to generate different build files.

We already have app.module.ts which was already generated by angular CLI. Modify it in such a way that it looks like the following:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';

import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { AboutComponent } from './components/about/about.component';
import { HomeComponent } from './components/home/home.component';


@NgModule({
    declarations: [
        AppComponent,
        AboutComponent,
        HomeComponent
    ],
    imports: [
        CommonModule,
        AppRoutingModule,
        HttpClientModule
    ],
    providers: []
})
export class AppModule { }

Now we’ll create the browser.app.module.ts file with the below content in the same directory of app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

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

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


@NgModule({
    imports: [
        AppModule,
        BrowserModule.withServerTransition({ appId: 'ssr-example' }),
    ],
    bootstrap: [AppComponent]
})
export class BrowserAppModule { }

So now, this module would be used by the browser and will render the application on the client side. BrowserModule.withServerTransition({ appId: ‘ssr-example’ }) line imports the BrowserModule with some configuration that tells angular that this is a server side rendered app. appId is the name of the app which has to be unique but it can be set to anything.

Similar to this, there would be one more module file called server.app.module.ts for the server.

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

import { AppModule } from './app.module';
import { AppRoutingModule } from './app-routing.module';

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


@NgModule({
    imports: [
        AppModule,
        BrowserModule.withServerTransition({ appId: 'ssr-example' }),
        ServerModule,
    ],
    bootstrap: [AppComponent]
})
export class ServerAppModule { }

Server module is the same as the browser module. But the only difference is that we need to add the ServerModule from @angular/platform-server module. If it isn’t present in the project then it can be downloaded or installed by the command: npm i –save @angular/platform-server.

2. Create the different entry points

We need different entry points for the modules that we created. Normally the default entry point created by CLI is app/main.ts. Similarly, there will be separate endpoints for both Browser and Server.

Hence, browser.main.ts will have content as follows:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';
import { BrowserAppModule } from './app/browser.app.module';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(BrowserAppModule)
    .catch(err => console.log(err));

And the server.main.ts will have the content as follows:

import { enableProdMode } from '@angular/core';
export { ServerAppModule } from './app/server.app.module';

enableProdMode();

3. Create tsconfig.json files

The tsconfig.app.json file is created by the CLI. Similarly we’ll have to create 2 different configurations. One for browser as browser.config.app.json that will have the content as follows:

{
    "extends": "../tsconfig.json",
    "angularCompilerOptions": {
        "entryModule": "./app/browser.app.module#BrowserAppModule"
    },
    "compilerOptions": {
        "outDir": "../out-tsc/browser",
        "baseUrl": "./",
        "module": "es2015",
        "types": []
    },
    "exclude": [
        "test.ts",
        "**/*.spec.ts"
    ]
}

And the other for the server as server.config.app.json with the below content.

{
    "extends": "../tsconfig.json",
    "angularCompilerOptions": {
        "entryModule": "./app/server.app.module#ServerAppModule"
    },
    "compilerOptions": {
        "outDir": "../out-tsc/server",
        "baseUrl": "./",
        "module": "commonjs",
        "types": []
},
    "exclude": [
        "test.ts",
        "**/*.spec.ts"
    ]
}

4. Modify ‘.angular-cli.json’ file

Now, we need to mNow, we need to modify the .angular-cli.json file to add an extra app entry for the server module. We also need to modify existing app entries to target renamed and new files.

{
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "project": {
        "name": "angular-ssr-example"
    },
    "apps": [
        {
            "platform": "browser",
            "root": "src",
            "outDir": "dist-browser",
            "assets": [
                "assets",
                "favicon.ico"
            ],
            "index": "index.html",
            "main": "browser.main.ts",
            "polyfills": "polyfills.ts",
            "test": "test.ts",
            "tsconfig": "browser.tsconfig.app.json",
            "testTsconfig": "tsconfig.spec.json",
            "prefix": "app",
            "styles": [
                "styles.scss"
            ],
            "scripts": [],
            "environmentSource": "environments/environment.ts",
            "environments": {
                "dev": "environments/environment.ts",
                "prod": "environments/environment.prod.ts"
            }
        },
        {
            "platform": "server",
            "root": "src",
            "outDir": "dist-server",
            "main": "server.main.ts",
            "tsconfig": "server.tsconfig.app.json",
            "testTsconfig": "tsconfig.spec.json",
            "environmentSource": "environments/environment.ts",
            "environments": {
                "dev": "environments/environment.ts",
                "prod": "environments/environment.prod.ts"
            }
        }
    ],
    "e2e": {
        "protractor": {
            "config": "./protractor.conf.js"
        }
    },
    "lint": [
        {
            "project": "src/tsconfig.app.json",
            "exclude": "**/node_modules/**"
        },
        {
            "project": "src/tsconfig.spec.json",
            "exclude": "**/node_modules/**"
        },
        {
            "project": "e2e/tsconfig.e2e.json",
            "exclude": "**/node_modules/**"
        }
    ],
    "test": {
        "karma": {
            "config": "./karma.conf.js"
        }
    },
    "defaults": {
        "styleExt": "scss",
        "component": {}
    }
}

In Server entry we don’t have polypills, styles, scripts as the server app refers to the browser app that already contains those resources.

5.  Create Distributions

Until now, what we have done is instruct Angular CLI on how to create distribution files for browser app and server app. It’s time to create distributions (builds) for these apps. To simplify, modify your package.json file to include these commands under scripts.

"scripts": {
    "build:browser": "ng build --prod --app 0",
    "build:server": "ng build --prod --app 1 --output-hashing none",
    "build": "npm run build:browser && npm run build:server",
    "serve": "node server.js"
},

To build a browser app, you need to use npm run build:browser and to build a server app, you need to use npm run build:server. But once you are done with some update in application, you should run only npm run build.

AfAfter running npm run build, you should see dist-browser and dist-server folders in project root.

6. Create Express Server

It’s time to create an express server which will render application HTML on the server. Create server.js file inside project root with below content.

// Angular requires Zone.js
require('zone.js/dist/zone-node');

const express = require('express');
const { ngExpressEngine } = require('@nguniversal/express-engine');

// create express app
const app = express();

// import server module bundle
var { ServerAppModuleNgFactory } = require('./dist-server/main.bundle');

// set up engine for .html file
app.engine('html', ngExpressEngine({
    bootstrap: ServerAppModuleNgFactory
}));

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

// server static files
app.use(express.static(__dirname + '/dist-browser', { index: false }));

// return rendered index.html on every request
app.get('*', (req, res) => {
    res.render('index', { req, res });
    console.log(`new GET request at : ${req.originalUrl}`);
});

// start server and listen
app.listen(3000, () => {
    console.log('Angular server started on port 3000');
});

Make sure to install @nguniversal/express-engin. This module is necessary to inject appropriate HTML inside app-root based on the route (request URL).

Apart from angular-specific code in server.js, other things should be pretty basic if you are a Node.js developer. To run the server, use the following command: npm run serve.

Parting Thoughts

So, this was an extensive guide on server-side rendering in Angular. With this, we are sure you would know how to create server-side rendering apps with Angular. ideal for deployment across industries like healthcare, entertainment, gaming, supply chain, retail and more, SPAs offer more benefits that what meet the eye.

Experiment with the unique features of Angular for your SPA OR Single page application project and get the market talking about your brand. Also, for more tutorials on Angular web development and Angular app development, keep watching this space. We believe this article has helped you.