Setup Angular Universal on AWS Lambda from scratch

Sahil Purav
Cactus Tech Blog
Published in
7 min readJan 5, 2020
Deploy Angular Universal on AWS Lambda with Serverless framework from scratch

Update: Thank you very much for your overwhelming response on original article. I’ve received a lot of request on social platforms for updating the article to support Angular 12+. So here you go… this article is an edited version of setting up Angular 10 universal setup on serverless framework.

Introduction

With the growing popularity of Single Page Applications, Angular (backed by Google) is one of the best and widely used frameworks among React, Vue, and others. While there are many benefits of using SPAs over traditional MPA (Multi-Page Applications), its process to render the DOM by executing JavaScript on browser causes poor performance on the first-page load and lacks SEO friendly feature (although it is improved in Google but not all search engines support SPA crawling).

Angular has a “Universal” package that solves the above specific problems by generating the static application page that later converts to a fully functional SPA page by bootstrapping at the client browser. It improves the first-page load drastically that gives users a chance to view some parts of the application view before it becomes fully functional. Since the title, meta, contents, etc are generated from the server, search engines easily understand and make them SEO friendly.

Angular Universal requires the page to be generated on the Server hence, in an ideal scenario, we would require a physical instance e.g. AWS EC2. Having a physical server increases infrastructure management as you may want to configure your servers with required packages and auto-scale based on the load.

In this article, we’re going to learn how to deploy your entire universal application on AWS Lambda using “Serverless Framework”. Serverless is a buzzword that has been on the rise for the past few years, and it doesn’t look like it’s going to stop, more companies are adopting it considering its benefits like “low cost”, “ease of deployment”, “infinite scalability”, “improved latency” and many more.

Why setting up from scratch?

Yes, there are plugins available to achieve this easily. But there are plenty of benefits by setting up “Serverless Angular Universal” from scratch:

  • Simulate lambda and API gateway on local
  • Easy termination of the entire infrastructure
  • More flexibility to customize the process
  • …and of course, you will learn the insights!

Prerequisites

The task requires you to have the following packages installed on the system:

If you don’t have any of the above prerequisites, please click on the links and follow the instructions to get started.

Once you installed AWS CLI, configure the access id and secret key:

aws configure

Apart from access id and secret key, the rest of the things are optional and you can leave it blank.

In a hurry?

I’ve created a https://github.com/sahilpurav/ngx-serverless-starter with all the required configuration to serve Angular Universal on the Serverless framework.

Set up an Angular project

We will begin by configuring the angular project and installing dependencies:

ng new ngx-serverless-starter --style scss --routing true
cd ngx-serverless-starter

Let’s test the application by hitting:

ng serve

Test your application by navigating to http://localhost:4200

Set up an Angular Universal

Next, we will install Angular Universal to support SSR (server-side rendering).

ng add @nguniversal/express-engine

The above command will install the necessary prerequisites and add new files to support the server-side rendering.

Run the following command to test your SSR application:

npm run dev:ssr

Test your application by navigating to http://localhost:4000. If you view the source of the page, you will see <app-root> has real HTML elements:

View Source of Angular Universal Page

Install dependencies for Serverless

We will be using the “aws-serverless-express” wrapper of express for creating the AWS Lambda function. We’re also installing the “Serverless” framework. It is a complete solution for building & operating on serverless infrastructure. It also has tools to simulate the Lambda on local that makes it easy for development.

npm install serverless serverless-offline -D
npm install @vendia/serverless-express --save

Configure Angular Universal to support Lambda

We will begin our serverless configuration by creating the serverless.yml file at the root directory.

serverless.yml

service: ngx-serverless-starter
plugins:
- serverless-offline
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
memorySize: 192
timeout: 10
package:
patterns:
- "!/**"
- "node_modules/@vendia/serverless-express/**"
- "dist/**"
- "lambda.js"
functions:
api:
handler: lambda.handler
events:
- http: GET /
- http: GET /{proxy+}

The “serverless-offline” will help us to simulate the Lambda on a local machine.

Understanding default “server” configuration

Angular Universal generates “server.ts” and “tsconfig.server.json” in an initial setup. If you open the angular.json file it has the following configuration for server setup:

"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/ngx-serverless-starter/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"inlineStyleLanguage": "scss"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,
"sourceMap": true,
"extractLicenses": false
}
},
"defaultConfiguration": "production"
},

If you hit ng run ngx-serverless-starter:server:production, “server.ts” file gets compiled to an “ngx-serverless-starter/server/main.js”.

Setting up our own serverless workflow

We will be following a similar workflow. Let’s copy the above configuration and edit it according to our new serverless configuration.

"serverless": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/ngx-serverless-starter/serverless",
"main": "serverless.ts",
"tsConfig": "tsconfig.serverless.json",
"inlineStyleLanguage": "scss"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,
"sourceMap": true,
"extractLicenses": false
}
},
"defaultConfiguration": "production"
},

I’ve updated the configuration name to “serverless”, changed the outputPath to dist/ngx-serverless-starter/serverless , and asked Angular to refer tsconfig.serverless.json and serverless.ts while running the build with this configuration.

Let’s create tsconfig.serverless.json file with the following configuration:

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

Further, let’s create serverless.ts file and add the following code:

import 'zone.js/dist/zone-node';import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
// The Express app is exported so that it can be used by serverless Functions.
export const server = express();
const distFolder = join(process.cwd(), 'dist/ngx-serverless-starter/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
export * from './src/main.server';

The whole code is copied from server.ts . If you compare these two files you would notice that I’ve removed the run function from as lambda function does not require any running port. I’ve also exported the express server variable.

lambda.js

Previously added serverless.yml the configuration has a “Lambda” function handler as follows:

functions:
api:
handler: lambda.handler

It expects the file called lambda.js with function named handler. Let’s create it:

const serverlessExpress = require("@vendia/serverless-express");
const app = require("./dist/ngx-serverless-starter/serverless/main");
const serverProxy = serverlessExpress.createServer(app.server);
module.exports.handler = (event, context) => serverlessExpress.proxy(serverProxy, event, context);

We’re using the “@vendia/serverless-express” wrapper that gets the express app instance from distribution to serve it on lambda. You can optionally set the MIME types can be different based on the project requirements. For more information on the setting visit the official documentation of the package.

Lastly, add .serverless inside .gitignore as we don’t want the temporary serverless output to be pushed on GIT.

Simulate lambda on local

We’ve now set up SSR with both “express server” and “serverless”. Add following lines in package.json to run lambda on local:

"dev:sls": "ng build -c development && ng run ngx-serverless-starter:serverless -c development && npm run serve:sls",
"serve:sls": "serverless offline start --noPrependStageInUrl"

Test the application by running:

npm run dev:sls

Just open http://localhost:3000 and whoa! you’ve successfully tested your application by simulating lambda on local 🚀

Deploy on AWS

The final step for us is to deploy the application on AWS Lambda. Since we’re using a serverless framework, it is easy to create AWS resources used for invoking Lambda functions. Just update the package.json with the following:

"deploy": "serverless deploy"

And run npm run build:sls && npm run deploy... It may take some time to package and upload your code but after a while, you will get the URL (https://{api-gateway-arn}.execute-api.{your-region}.amazonaws.com/dev) on the CLI, just hit it and enjoy looking at Angular served from Lambda ☁️ ⚡️

You should further have a domain mapped with this URL to run the application smoothly without the “dev” suffix.

Terminate Infrastructure

If you wish to terminate the infrastructure generated by this article, add the following line of code in package.json

"terminate": "serverless remove"

Running npm run terminate will delete all the AWS resources created for your project.

--

--

Sahil Purav
Cactus Tech Blog

Associate Director at Cactus Communications, India. I help building highly scalable architecture for Authors and help them to publish their papers