How to Implement Server-Side Rendering in Angular 17 Using the Serverless Framework and NX

Fernando Gomez Profile Picture

Fernando Gomez

May 20, 2024 · 5 minutes read

Introduction

In this guide, we’ll explore setting up Angular 17 SSR (Server Side Rendering) with the serverless framework. We’ll be using the NX framework, which I personally find invaluable due to its robust set of tools. Also, we won’t be deploying this project to AWS in this tutorial, but don’t worry—I’ll cover that in an upcoming post. Let’s get started!

Create NX Workspace

To start generating an NX workspace, use the following command:

npx create-nx-workspace@latest

You’ll be prompted to choose a name for your workspace.

After selecting a name for your workspace, you’ll be prompted to choose the technology stack you want to use. In our case, we’ll select Angular.

Next, you’ll need to decide whether you prefer a standalone project or a monorepo structure. I’ll choose standalone for this project since I only plan to have one app. However, if you’re interested in managing multiple apps within the same workspace, opting for a monorepo would be the way to go.

After choosing your project structure, you’ll be asked to select a bundler. For this project, I will opt for esbuild because of its speed. However, if you need more advanced features for bundling, consider choosing webpack, as it provides a broader range of capabilities.

Then, you’ll choose the stylesheet format you prefer for your project.

Next, you will be prompted to decide if you want NX to configure your Angular app for Server Side Rendering (SSR). This contributes to NX’s popularity—it simplifies complex configurations, making our development process smoother and more efficient.

Subsequently, you’ll be prompted to select an e2e (end-to-end) test runner. For this project, I will choose ‘none’ as I do not plan to use it.

Next, you’ll need to decide whether to use the NX Cloud. This decision is entirely up to you. If you’re not familiar with what NX Cloud offers, I recommend visiting their website to learn about its features and benefits.

After you decide whether to use the NX Cloud, the workspace generation will begin, and all the npm dependencies will be installed. Once the setup is complete, navigate into your workspace directory using the cd command and you’ll see your new project structure laid out.

Create a Simple Hello World App

Once the workspace has been generated, create a simple “Hello World” app by editing the app.component.ts file.

import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
  standalone: true,
  imports: [RouterModule],
  selector: 'app-root',
  template: `Hello World! From The Server!`,
})
export class AppComponent {
}

Next, run the following command in your terminal:

npm start

After running the command, navigate to http://localhost:4200/ in your web browser. You should see the following display:

Note: At this point, our app is being rendered on the client, not on the server.

Install Serverless NPM Dependencies

At this stage, NX has configured our Angular application for server-side rendering. To avoid the high costs associated with deploying on an EC2 instance, we’ll opt for a Lambda function. The Serverless framework will facilitate this transition. To begin, install the required npm dependencies by executing the following command in the terminal.

npm i @codegenie/serverless-express aws-serverless-express serverless serverless-apigw-binary serverless-associate-waf serverless-offline

@codegenie/serverless-express & aws-serverless-express enables the running of Express atop AWS Lambda and Amazon API Gateway.

serverless is a framework designed for building applications on AWS Lambda and other next-generation cloud services. It features auto-scaling and charges only when the services are running. This significantly reduces the total cost of operation, allowing you to focus more on development and less on management.

serverless-apigw-binary is a plugin that automates the process of enabling support for binary files in Amazon API Gateway.

serverless-associate-waf is a plugin that allows you to associate a regional Web Application Firewall (WAF) with AWS API Gateway. This firewall protects against attacks such as cross-site scripting (XSS), DDoS, and cookie manipulation, helping to secure your application.

serverless-offline is a plugin that emulates AWS Lambda and API Gateway on your local environment, accelerating the development process by allowing you to test changes directly on your machine.

Create A Project Serverless Configuration

To start, add a serverless.ts file in the project root to define an Express server. The content of the file should be as follows:

import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import bootstrap from './src/main.server';

export function app(): express.Express {
	const server = express();
	const distFolder = join(process.cwd(), 'dist/{YOUR ORG}/browser'); // Change it for your org name.
	const indexHtml = existsSync(join(distFolder, 'index.original.html'))
		? join(distFolder, 'index.original.html')
		: join(distFolder, 'index.html');

	const commonEngine = new CommonEngine();

	server.set('view engine', 'html');
	server.set('views', distFolder);

	server.get(
		'*.*',
		express.static(distFolder, {
			maxAge: '1y',
		}),
	);

	server.get('*', (req, res, next) => {
		const { protocol, originalUrl, baseUrl, headers } = req;

		commonEngine
			.render({
				bootstrap,
				documentFilePath: indexHtml,
				url: `${protocol}://${headers.host}${originalUrl}`,
				publicPath: distFolder,
				providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
			})
			.then((html) => res.send(html))
			.catch((err) => next(err));
	});

	return server;
}

function run(): void {
	const port = process.env['PORT'] || 4000;

	const server = app();
	server.listen(port, () => {
		console.log(`Node Express server listening on http://localhost:${port}`);
	});
}

// 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')) {
	run();
}

export default bootstrap;

Next, create a TypeScript configuration file for the serverless configuration. Name this file tsconfig.serverless.json and ensure it looks like this:

{
	"extends": "./tsconfig.app.json",
	"compilerOptions": {
		"outDir": "../../out-tsc/serverless",
		"target": "es2019",
		"types": ["node"]
	},
	"files": ["src/main.server.ts", "serverless.ts"]
}

After adding the TypeScript Serverless configuration, open the project.json file located in the project root. Find the targets property and add a new property called serverless with the following content:

"serverless": {
      "dependsOn": ["build"],
      "executor": "@angular-devkit/build-angular:server",
      "options": {
        "outputPath": "dist/{YOUR ORG}/serverless", // Change it for your org name.
        "main": "serverless.ts",
        "tsConfig": "tsconfig.serverless.json",
        "inlineStyleLanguage": "scss"
      },
      "configurations": {
        "production": {
          "outputHashing": "media"
        },
        "development": {
          "buildOptimizer": false,
          "optimization": false,
          "sourceMap": true,
          "extractLicenses": false,
          "vendorChunk": true
        }
      },
      "defaultConfiguration": "production"
    }

Now, add the Lambda handler by creating a file named lambda.js in your project root. The file should contain:

const awsServerlessExpress = require('aws-serverless-express');
const server = require('./dist/{YOUR ORG}/serverless/main'); // Change it for your org name.
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');

const binaryMimeTypes = [
	'application/javascript',
	'application/json',
	'application/octet-stream',
	'application/xml',
	'image/jpeg',
	'image/png',
	'image/gif',
	'image/webp',
	'text/comma-separated-values',
	'text/css',
	'text/html',
	'text/javascript',
	'text/plain',
	'text/text',
	'text/xml',
	'image/x-icon',
	'image/svg+xml',
	'application/x-font-ttf',
];

const app = server.app();
app.use(awsServerlessExpressMiddleware.eventContext());
const serverProxy = awsServerlessExpress.createServer(app, null, binaryMimeTypes);

module.exports.universal = (event, context) => awsServerlessExpress.proxy(serverProxy, event, context);

Finally, create a serverless.yml file in the project root with the following configuration:

service: project-serverless # Choose your project name. It is up to you.

useDotenv: true

plugins:
  - serverless-apigw-binary
  - serverless-offline
  - serverless-associate-waf

provider:
  name: aws
  runtime: nodejs18.x
  memorySize: 2048
  timeout: 10
  region: us-east-1
  apiGateway: 
   minimumCompressionSize: 1024

package:
  excludeDevDependencies: true
  patterns:
    - '!.angular/**'
    - '!.github/**'
    - '!.vscode/**'
    - '!coverage/**'
    - '!libs/**'
    - '!src/**'
    - '!node_modules/**'
    - '!e2e/**'
    - '!firebug-lite/**'
    - '!.eslintignore'
    - '!.eslintrc.base.json'
    - '!.eslintrc.json'
    - '!.gitignore'
    - '!.prettierignore'
    - '!.prettierrc'
    - '!jest.config.app.ts'
    - '!jest.config.ts'
    - '!jest.preset.js'
    - '!nx.json'
    - '!README.md'
    - '!server.ts'
    - '!tailwind.config.js'
    - '!tsconfig.app.json'
    - '!tsconfig.base.json'
    - '!tsconfig.editor.json'
    - '!tsconfig.json'
    - '!tsconfig.server.json'
    - '!tsconfig.spec.json'
    - node_modules/@vendia/**
    - node_modules/aws-serverless-express/**
    - node_modules/binary-case/**
    - node_modules/type-is/**
    - node_modules/media-typer/**
    - node_modules/mime-types/**
    - node_modules/mime-db/**

custom:
  serverless-offline:
    noPrependStageInUrl: true
    printOutput: true
  contentCompression: 1024
  apigwBinary:
    types:
      - '*/*'

functions:
  api:
    handler: lambda.universal
    events:
      - http: ANY /{proxy+}
      - http: ANY /

Testing Our Implementation Locally

Before testing the implementation, add some helpful scripts to your package.json file. Open package.json and add the following scripts section:

"start:serverless": "nx run {YOUR ORG}:serverless && serverless offline start",
"build:serverless": "nx run {YOUR ORG}:serverless"

The first script will compile your code and emulate AWS Lambda and API Gateway on your local environment.

The second script will just compile your code.

Now, let’s test our implementation locally by running:

npm run start:serverless

Our local Serverless server will start on http://localhost:3000/

Conclusion

Implementing server-side rendering (SSR) in Angular 17 with the Serverless Framework and NX offers a robust solution for enhancing your application’s performance and SEO. This guide has provided a comprehensive setup, from creating an NX workspace and configuring SSR to deploying with AWS Lambda. However, deploying the application requires further steps not covered here. By following these initial instructions, you have laid a strong foundation for a scalable and efficient Angular application.

Link to the repo: https://github.com/Fernandocgomez/how-to-implement-server-side-rendering-in-angular-17-using-the-serverless-framework-and-nx