Chapter 15. Server-side Rendering with Angular Universal

If you are not familiar with the Angular platform for client-side application development it is worth taking a look into. Nest.js has a unique symbiotic relationship with Angular because they are both written in TypeScript. This allows for some interesting code sharing between your Nest.js server and Angular app, because Angular and Nest.js both use TypeScript, and classes can be created in a package that is shared between the Angular app and the Nest.js app. These classes can then be included in either app and help keep the objects that are sent and received over HTTP requests between client and server consistent. This relationship is taken to another level when we introduce Angular Universal. Angular Universal is a technology that allows your Angular app to be pre-rendered on your server. This has a number of benefits such as:

  1. Facilitate web crawlers for SEO purposes.
  2. Improve the load performance of your site.
  3. Improve performance of the site on low-powered devices and mobile.

This technique is called server-side rendering and can be extremely helpful, but, requires some restructuring of the project as the Nest.js server and Angular app are built in sequence and the Nest.js server will actually run the Angular app itself when a request is made to get a webpage. This essentially emulates the Angular app inside a browser complete with the API calls and loading any dynamic elements. This page built on the server is now served as a static webpage to the client and the dynamic Angular app is loaded quietly in the background.

If you are just hopping into this book now and want to follow along with the example repository it can be cloned with:

git clone https://github.com/backstopmedia/nest-book-example

Angular is another topic that can, and has, have an entire book written about it. We will be using an Angular 6 app that has been adapted for use in this book by one of the authors. The original repository can be found here.

https://github.com/patrickhousley/nest-angular-universal.git

This repository is using Nest 5 and Angular 6 so there have been some changes made, because this book is based on Nest 4. Not to worry, though, we have included an Angular Universal project inside the main repository shown at the start of this chapter. It can be found inside the universal folder at the root of the project. This is a self-contained Nest + Angular project, rather than adapting the main repository for this book to serve an Angular app, we isolated it to provide a clear and concise example.

Serving the Angular Universal App with Nest.js

Now that we are going to be serving the Angular app with our Nest.js server, we are going to have to compile them together so that when our Nest.js server is run, it knows where to look for the Universal app. In our server/src/main.ts file there are a couple of key things we need to have in there. Here we create a function bootstrap() and then call it from below.

async function bootstrap() {
  if (environment.production) {
    enableProdMode();
  }

  const app = await NestFactory.create(ApplicationModule.moduleFactory());

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }

  await app.listen(environment.port);
}

bootstrap()
  .then(() => console.log(`Server started on port ${environment.port}`))
  .catch(err => console.error(`Server startup failed`, err));

Let’s step through this function line by line.

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

This tells the application to enable production mode for the application. There are many differences between production and development modes when writing web servers, but this is required if you want to run a web server in a production context.

const app = await NestFactory.create(ApplicationModule.moduleFactory());

This will create the Nest app variable of type INestApplication and will be run using ApplicationModule in the app.module.ts file as the entry point. app will be the instance of the Nest app that is running on port environment.port, which can be found in src/server/environment/environment.ts. There are three different environment files here:

  1. environment.common.ts-As its name implies, this file is common between both production and development builds. It provides information and paths on where to find the packaged build files for the server and client applications.
  2. environment.ts-This is the default environment used during development, and it includes the settings from the environment.common.ts file as well as sets production: false and the port mentioned above to 3000.
  3. environment.prod.ts-This file mirrors #2 except that it sets production: true and does not define a port, instead defaulting to default, normally 8888.

If we are developing locally and want to have hot reloading, where the server restarts if we change a file, then we need to have the following included in our main.ts file.

if (module.hot) {
  module.hot.accept();
  module.hot.dispose(() => app.close());
}

This is set within the webpack.server.config.ts file based on our NODE_ENV environment variable.

Finally, to actually start the server, call the .listen() function on our INestApplication variable and pass it a port to run on.

await app.listen(environment.port);

We then call bootstrap(), which will run the function described above. At this stage we now have our Nest server running and able to serve the Angular App and listen to serve API requests.

In the bootstrap() function above when creating the INestApplication object we supplied it with ApplicationModule. This is the entry point for the app and handles both the Nest and Angular Universal Apps. In app.module.ts we have:

@Module({
  imports: [
    HeroesModule,
    ClientModule.forRoot()
  ],
})
export class ApplicationModule {}

Here we are importing two Nest modules, the HeroesModule, which will supply the API endpoints for the Tour of Heroes application, and the ClientModule that is the module that is handling the Universal stuff. The ClientModule has a lot going on, but we will touch on the main things that are handling setting up Universal, here is the code for this module.

@Module({
  controllers: [ClientController],
  components: [...clientProviders],
})
export class ClientModule implements NestModule {
  constructor(
    @Inject(ANGULAR_UNIVERSAL_OPTIONS)
    private readonly ngOptions: AngularUniversalOptions,
    @Inject(HTTP_SERVER_REF) private readonly app: NestApplication
  ) {}

  static forRoot(): DynamicModule {
    const requireFn = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
    const options: AngularUniversalOptions = {
      viewsPath: environment.clientPaths.app,
      bundle: requireFn(join(environment.clientPaths.server, 'main.js'))
    };

    return {
      module: ClientModule,
      components: [
        {
          provide: ANGULAR_UNIVERSAL_OPTIONS,
          useValue: options,
        }
      ]
    };
  }

  configure(consumer: MiddlewareConsumer): void {
    this.app.useStaticAssets(this.ngOptions.viewsPath);
  }
}

We will start with the @Module decorator at the top of the file. As with regular Nest.js modules (And Angular, remember how Nest.js is inspired by Angular?), there are controllers (for the endpoints) property and a components (for services, providers and other components we want to be part of this module) property. Here we are including the ClientController in the controllers array and ...clientProviders in components. Here the triple dot (...) essentially means “insert each of the array elements into this array.” Let’s disect each of these a bit more.

ClientController

@Controller()
export class ClientController {
  constructor(
    @Inject(ANGULAR_UNIVERSAL_OPTIONS) private readonly ngOptions: AngularUniversalOptions,
  ) { }

  @Get('*')
  render(@Res() res: Response, @Req() req: Request) {
    res.render(join(this.ngOptions.viewsPath, 'index.html'), { req });
  }
}

This is like any other controller that we have learned about, but with one small difference. At the URL path /* instead of supplying an API endpoint, the Nest.js server will render an HTML page, namely index.html, from that same viewsPath we have seen before in the environment files.

As for the clientProoviders array:

export const clientProviders = [
  {
    provide: 'UNIVERSAL_INITIALIZER',
    useFactory: async (
      app: NestApplication,
      options: AngularUniversalOptions
    ) => await setupUniversal(app, options),
    inject: [HTTP_SERVER_REF, ANGULAR_UNIVERSAL_OPTIONS]
  }
];

This is similar to how we define our own provider inside the return statement of ClientModule, but instead of useValue we use useFactory, this passes in the Nest app and the AngularUniversalOptions we defined earlier to a function setupUniversal(app, options). It has taken us a while, but this is where the Angular Universal server is actually created.

setupUniversal(app, options)

export function setupUniversal(
  app: NestApplication,
  ngOptions: AngularUniversalOptions
) {
  const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = ngOptions.bundle;

  app.setViewEngine('html');
  app.setBaseViewsDir(ngOptions.viewsPath);
  app.engine(
    'html',
    ngExpressEngine({
      bootstrap: AppServerModuleNgFactory,
      providers: [
        provideModuleMap(LAZY_MODULE_MAP),
        {
          provide: APP_BASE_HREF,
          useValue: `http://localhost:${environment.port}`
        }
      ]
    })
  );
}

There are three main functions being called here: app.setViewEngine(), app.setBaseViewDir(), and an app.engine. The first .setViewEngine() is setting the view engine to HTML so that the engine rendering views knows we are dealing with HTML. The second .setBaseViewDir() is telling Nest.js where to find the HTML views, which again was defined in the environment.common.ts file from earlier. The last is very important, .engine() defines the HTML engine to use, in this case, because we are using Angular, it is ngExpressEngine, which is the Angular Universal engine. Read more about the Universal express-engine here: https://github.com/angular/universal/tree/master/modules/express-engine. This sets bootstrap to the AppServerModuleNgFactory object, which is discussed in the next section.

In the ClientModule we can see the .forRoot() function that was called when we imported the ClientModule in the AppliationModule (server entry point). Essentially, forRoot() is defining a module to return in place of the originally imported ClientModule, also called ClientModule. This module being returned has a single component that provides ANGULAR_UNIVERSAL_OPTIONS, which is an interface that defines what kind of object will be passed into the useValue property of the component.

The structure of ANGULAR_UNIVERSAL_OPTIONS is:

export interface AngularUniversalOptions {
  viewsPath: string;
  bundle: {
    AppServerModuleNgFactory: any,
    LAZY_MODULE_MAP: any
  };
}

It follows that the value of useValue is the contents of options defined at the top of forRoot().

const options: AngularUniversalOptions = {
  viewsPath: environment.clientPaths.app,
  bundle: requireFn(join(environment.clientPaths.server, 'main.js'))
};

The value of environment.clientPaths.app can be found in the environment.common.ts file we discussed earlier. As a reminder, it points to where to find the compiled client code to be served. You may be wondering why the value of bundle is a require statement when the interface clearly says it should be of the structure:

bundle: {
    AppServerModuleNgFactory: any,
    LAZY_MODULE_MAP: any
  };

Well, if you trace that require statement back (.. means go up one directory) then you will see we are setting the bundle property equal to another module AppServerModule. This will be discussed in a bit, but the Angular App will end up being served.

The last piece in the ClientModule is in the configure() function that will tell the server where to find static assets.

configure(consumer: MiddlewareConsumer): void {
    this.app.useStaticAssets(this.ngOptions.viewsPath);
  }

Building and running the Universal App

Now that you have the Nest.js and Angular files setup, it is almost time to run the project. There are a number of configuration files that need your attention and are found in the example project: https://github.com/backstopmedia/nest-book-example. Up until now we have been running the project with nodemon so that our changes are reflected whenever the project is saved, but, now that we are packaging it up to be serving an Angular App we need to build the server using a different package. For this, we have chosen udk, which is a webpack extension. It can both build our production bundles as well as start a development server, much like nodemon did for our plain Nest.js app. It would be a good idea to familiarize yourself with the following configuration files:

  1. angular.json-Our Angular config file that handles things like, what environment file to use, commands that can be used with ng, and the Angular CLI command.
  2. package.json-The project global dependency and command file. This file defines what dependencies are needed for production and development as well as what commands are available for your command line tool, ie. yarn or npm.
  3. tsconfig.server.json-An extension on the global tsconfig.json file, this provides some Angular Compiler options like where to find the Universal entry point.

Summary

And that is it! We have a working Angular Universal project to play around with. Angular is a great client side framework that has been gaining a lot of ground lately. There is much more that can be done here as this chapter only scratched the surface, especially in terms of Angular itself.

And, this is the last chapter in this book. We hope you are excited to use Nest.js to create all sorts of apps.