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 usecurl
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 fromserver/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 inapps/backend/src/app/app.module.ts
, you can copy the lines fromserver/app.module.ts
. Remember to fix the import path ofAppServerModule
.
// 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 withcurl localhost:4200
. NestJS adds the/api
prefix for the api requests. Make a request tolocalhost: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🧑🏼💻