TL;DR
- Scaffold a Nx workspace with an Angular and NestJS app
nx-workspace@latest --preset angular-nest
- 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
- 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
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')
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
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
...
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
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 { }
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");
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 { }
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';
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
}
},
...
}
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"
}
},
...
}
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"
},
...
}
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"
@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)
How can I run the init generator after
yarn add @nxarch/ng-nest
? It seems there is missing something in this command: