DEV Community

Yannick Boetzkes
Yannick Boetzkes

Posted on • Updated on

Fullstack JS with Angular Universal and NestJS (Nx flavor)

TL;DR

  • Scaffold a Nx workspace with an Angular and NestJS app
nx-workspace@latest --preset angular-nest
Enter fullscreen mode Exit fullscreen mode
  • Install @nxarch/ng-nest and use the init schematic to generate the necessary project setup
yarn add @nxarch/ng-nest

@nxarch/ng-nest:init --ssrApp=my-angular-project --serverApp=my-nestjs-project
Enter fullscreen mode Exit fullscreen mode
  • Use yarn dev:server to start the app

Side note:
Make sure to use one of these environment variables for local development.

APP_ENV=development 
#or 
REMOVE_WEBPACK_CACHE=true
Enter fullscreen mode Exit fullscreen mode

If you're using relative paths for your server side API calls via the Angular HttpClient you might encounter an error as it doesn't prepend the base path.

There are multiple ways to solve this:

  • Use an HttpInterceptor that prepends the base url only on the server
  • or update the PlatformConfig to prepend relative paths with the base path
  • or just replace relative paths in your HttpClient calls with absolute paths
hello$ = this.http.get('http://localhost:4200/api/hello')
Enter fullscreen mode Exit fullscreen mode

Angular and NestJS share a lot of common concepts and design patterns. You can even go as far as sharing services if you keep them framework agnostic. As a developer being familiar with either of the frameworks you will probably feel comfortable pretty quickly.

But why would you use Angular Universal?
There are a lot of use cases for server side rendered applications such as:

  • Improved SEO performance
  • Improved load performance of your application
  • Shortened time to first meaningful paint and perceived time to interactive. Your users will thank you.

Angular Universal itself isn’t a new framework you’d have to learn. It’ll just enhance your apps capabilities as it will also run on the server. Eventually it will send the rendered html to the client.

Here are some (non-exhaustive) things you can do on the server within your Angular application when Angular Universal is used.

  • Accessing the request object in your Angular business logic
  • Caching responses
  • Issue requests from localhost to localhost (fast)
  • Getting access to session values in your Angular app

In order to use Angular Universal with a simple express server you can just use

ng add @nguniversal/express-engine
Enter fullscreen mode Exit fullscreen mode

But you’re here to read about how to setup Angular Universal with a NestJS server.

There is a great library created by the NestJS team that helps integrating an Angular Universal app with a NestJS app. It will compile the NestJS app together with the Angular SSR app in one bundle.

👉 This article will show how to setup a Nx workspace with a compiled output of three separate bundles -> one for the server, one for the ssr app and one for the client side bundle.

This yields the advantage that we only need to recompile those bundles that are affected by code changes. Hence, it will decrease compile time significantly in bigger code bases.

One possible dist folder structure looks like this.

├── dist
│   ├── server
│   |   ├── main.js
│   ├── ssr-app
│   |   ├── main.js
│   ├── ui-app
│   |   ├── main.js
│   |   ├── index.html
...
Enter fullscreen mode Exit fullscreen mode

We also want to use browser-sync during local development in order to proxy our server and enable live refreshes whenever the NestJS app or the Angular app is rebuild.

Let’s explore what the scaffolded projects look like after we’ve used the init generator

yarn add @nxarch/ng-nest

@nxarch/ng-nest:init --ssrApp=my-angular-project --serverApp=my-nestjs-project
Enter fullscreen mode Exit fullscreen mode

The scaffolded Projects

Now there are three app module files

The AppBrowserModule, the AppSsrModule and the server/AppModule.

// server/app.module.ts

@Module({
  imports: [
    AngularUniversalModule.forRoot({
      bootstrap: join(process.cwd(), 'dist/apps/ui/ssr/main.js'),
      viewsPath: join(process.cwd(), 'dist/apps/ui/browser'),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

Enter fullscreen mode Exit fullscreen mode

The server AppModule will import the AngularUniversalModule which is responsible for setting up server-side-rendering routes and delegating requests to the ngExpressEngine.

For more options see the nxarch/nest-nguniversal repository. You can configure options such as using custom endpoints for render routes, asynchronous cache storages or the render path.

If you keep the default wildcard render path make sure to prefix the api paths. We do this in your main.ts file.

// server/main.ts

app.setGlobalPrefix("/api");

Enter fullscreen mode Exit fullscreen mode

The AppSsrModule is only loaded and instantiated on the server. It uses the AppBrowserModule in order to render our html document. The AppBrowserModule is the main Angular module that’s also used on the client side.

// ui/app.ssr.module.ts

@NgModule({
  imports: [
    ServerModule,
    AppBrowserModule
  ],
  bootstrap: [AppComponent],
})
export class AppSsrModule { }

Enter fullscreen mode Exit fullscreen mode

main.ssr.ts

The path of the compiled output of this file is passed via the bootstrap option of the AngularUniversalModule options. We want to avoid to use any Angular specific dependencies in our NestJS bundle. That’s why we need to re-export the ngExpressEngine here.

// ui/main.ssr.ts

import'@angular/platform-server/init';
import{enableProdMode}from'@angular/core';
import{environment}from'./environments/environment';

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

export{AppSsrModule}from'./app/app.ssr.module';
export{ngExpressEngine}from'@nguniversal/express-engine';
Enter fullscreen mode Exit fullscreen mode

server/project.json

There are probably two additions worth mentioning.

The server project.json needs some external dependencies to be set in order to tell Webpack not to bundle those packages. This will prevent warnings and errors.

"targets": {
        ...
    "build": {
        "executor": "@nrwl/webpack:webpack",
        "outputs": [
          "{options.outputPath}"
        ],
        "options": {
          "target": "node",
          "compiler": "tsc",
          "outputPath": "dist/apps/server",
          "main": "apps/server/src/main.ts",
          "tsConfig": "apps/server/tsconfig.app.json",
          "assets": [
            "apps/server/src/assets"
          ],
          "externalDependencies": [
            "@nestjs/common",
            "@nestjs/core",
            "express",
            "@nestjs/microservices",
            "@nestjs/microservices/microservices-module",
            "@nestjs/websockets",
            "@nestjs/websockets/socket-module",
            "cache-manager"
          ],
          "optimization": false
        }
    },
    ...
}

Enter fullscreen mode Exit fullscreen mode

Also there’s another target added that can be used to serve the application during local development.

"targets": {
    ...
    "serve-ssr": {
    "executor": "@nxarch/ng-nest:build",
    "options": {
      "browserTarget": "ui:build:development",
      "ssrTarget": "ui:ssr:development",
      "serveTarget": "api:serve:development"
    }
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

ui/project.json

In the ui project.json one additional target for the server side rendered app has been added.

"targets": {
    "ssr": {
      "executor": "@angular-devkit/build-angular:server",
      "options": {
        "outputPath": "dist/apps/ui/ssr",
        "main": "apps/ui/src/main.ssr.ts",
        "tsConfig": "apps/ui/tsconfig.ssr.json",
        "inlineStyleLanguage": "scss",
        "outputHashing": "none",
        "optimization": false
      },
      "configurations": {...},
      "defaultConfiguration": "production"
    },
    ...
}
Enter fullscreen mode Exit fullscreen mode

package.json

Last but not least notice there’s one more script in our scripts object which we can use to start the dev server.

"dev:server": "nx serve-ssr api"
Enter fullscreen mode Exit fullscreen mode

@nxarch/ng-nest tries to make integrating and setting up an Angular Universal app and a NestJS app quick, simple and more productive.

If you enjoy using it don't forget to star ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️.

Top comments (1)

Collapse
 
dtslvr profile image
Thomas Kaul

How can I run the init generator after yarn add @nxarch/ng-nest? It seems there is missing something in this command:

@nxarch/ng-nest:init --ssrApp=my-angular-project --serverApp=my-nestjs-project
Enter fullscreen mode Exit fullscreen mode