NestJS Angular Universal in an Nx Workspace

NestJS Angular Universal in an Nx Workspace

Not so long ago I started using Angular Universal in all my Angular projects because of its benefits in SEO and performance on low-end devices and slow connections. I've been using Nx since a while and one day I started an Angular application that would be connecting to a NestJS API, so I thought that I should use the official NestJS package for Angular Universal: @nestjs/ng-universal.

The steps are the following:

  • Create an Angular application
nx generate @nrwl/angular:application --name=frontend
  • Add NestJS Schematics to workspace
yarn add -D @nrwl/nest # or npm i -D @nrwl/nest
  • Create the NestJS application
nx generate @nrwl/nest:application --name=backend --no-interactive
  • Now we have to add NestJS Universal Package
nx add @nestjs/ng-universal --clientProject=frontend

So far so good, our project structure should look like this:

apps
––backend/
––frontend/
.
.
.
.
server/ # Added by @nestjs/ng-universal
  • Next step is to run the application
yarn dev:ssr
# frontend is default project, else run
# nx run <projectName>:dev-ssr
  • Open localhost:4200 in browser and you'll see the app running. You can use curl in terminal and you'll see that is server rendered
curl localhost:4200

Excellent! but there's a problem, Angular is using the default server file when adding universal support by using ng add ng-universal/express-engine. That's because NestJS schematic doesn't recognize if the application is under an Nx Workspace. Let's fix that:

  • Replace apps/backend/src/main.ts with the content from server/main.ts and change the import path of AppModule.
// apps/backend/src/main.ts

import { NestFactory } from '@nestjs/core';
// import { AppModule } from './app.module'; * Before
import { AppModule } from './app/app.module'; // * After

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api');
  await app.listen(process.env.PORT || 4000);
}

// 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')) {
  bootstrap().catch(err => console.error(err));
}
  • Add the AngularUniversalModule to the imports array in apps/backend/src/app/app.module.ts, you can copy the lines from server/app.module.ts. Remember to fix the import path of AppServerModule.
// apps/backend/src/app/app.module.ts

import { Module } from '@nestjs/common';
import { AngularUniversalModule } from '@nestjs/ng-universal';
import { join } from 'path';
// import { AppServerModule } from '../src/main.server'; * Before

// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import { AppServerModule } from './../../../frontend/src/app/app.server.module'; // * After

@Module({
  imports: [
        // Added import
    AngularUniversalModule.forRoot({
      bootstrap: AppServerModule,
      viewsPath: join(process.cwd(), 'dist/frontend/browser')
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule { }
  • Finally, open apps/frontend/server.tsdelete all the content except import 'zone.js/dist/zone-node'; and `export * from './src/main.server'; (first and last lines). The file should look like this.
// apps/frontend/src/server.ts

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

// * Add this lines
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import './../backend/src/main';

export * from './src/main.server';
  • Run the app again
[Nest] 19734   - 01/31/2021, 4:49:59 PM   [NestFactory] Starting Nest application...

[Nest] 19734   - 01/31/2021, 4:49:59 PM   [InstanceLoader] AppModule dependencies initialized +33ms

[Nest] 19734   - 01/31/2021, 4:49:59 PM   [InstanceLoader] AngularUniversalModule dependencies initialized +1ms

[Nest] 19734   - 01/31/2021, 4:49:59 PM   [NestApplication] Nest application successfully started +6ms

** Angular Universal Live Development Server is listening on http://localhost:4200, open your browser on http://localhost:4200 **
Angular is running in development mode. Call enableProdMode() to enable production mode.
  • Visit localhost:4200 again and you'll see the app running! You can verify again that is server rendered with curl localhost:4200. NestJS adds the /api prefix for the api requests. Make a request to localhost:4200/api and the NestJS controller will respond:
curl localhost:4200/api

# Response
{"message":"Welcome to backend!"}

That's all! Now you've added Angular Universal to a NestJS Application in an Nx Workspace!

Here's the repo with the source code:

Disclaimer: This is my first blog post🧑🏼‍💻