Getting Started with Serverless Angular Universal on AWS Lambda

Maciej Treder - Sep 19 '19 - - Dev Community

Originally published on the Twilio Blog

You can build search-engine optimized (SEO) friendly single page applications (SPA’s) with Angular Universal, a technology that runs your Angular application on the server. And you can reduce the cost of running those applications with AWS Lambda, an event-driven, serverless computing platform provided as a part of Amazon Web Services (AWS). This post will show you how to build and deploy Angular Universal projects on AWS Lambda using Serverless Framework, an open-source command line interface for building and deploying serverless applications.

In this post we will:

  • Create an Angular application that contains two routes and makes calls to an external API
  • Add server-side rendering for SEO purposes
  • Set up a Serverless Framework framework configuration
  • Deploy the app on AWS Lambda

To accomplish the tasks in this post you will need to create an AWS account and install the following:

To configure the AWS CLI you’ll need the following from your AWS account:

  • Access Key ID
  • Secret Key
  • Default region

Your root user will have all the rights necessary to deploy and run the Angular app as a Lambda function. You can get an Access Key ID and a Secret Key for your root user by generating a new pair in the Identity and Access Management (IAM) console.

If you do not want to use your root user ID for this project you can Set Up an IAM Role and Policy for an API to Invoke Lambda Functions. This process has a number of steps and is not recommended if you are new to AWS.

Be sure you have completed all these tasks successfully before proceeding.

Set up the Angular project and run Hello World!

The first step we must take in every Angular project is initialization and installation of the package:

ng new angularSeo --style css --routing false
cd angularSeo
Enter fullscreen mode Exit fullscreen mode

Let’s make small changes with the generated project. First, let’s add some style to the <app-root> in the src/index.html file. Replace the existing code with the following:

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>AngularSeo</title>
 <base href="/">

 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
 <app-root class="content"></app-root>
 <footer>Built with <a href="https://github.com/maciejtreder/ng-toolkit">ng-toolkit</a> by <a href="https://www.maciejtreder.com">maciejtreder.com</a></footer>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Next, replace the existing content of the src/app/app.component.html with:

<h1>Hello World!</h1>
Enter fullscreen mode Exit fullscreen mode

Finally, add the following to src/styles.css:

body {
   margin: 0 auto;
   max-width: 1000px;
   background: url('assets/img/sandbox.png') no-repeat center;
   display: flex;
   flex-direction: column;
   height: 100%;
   font-family: 'Source Sans Pro', calibri, Arial, sans-serif !important;
   min-height: 550px;
}
.content {
   flex: 1 0 auto;
}
footer {
   padding: 10px 0;
   text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

Note, in the above, that we are using my favorite fancy background:

Background

Download it, and place it in src/assets/img catalog.

Let’s run the application by typing:

ng serve

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

Date: 2018-10-29T08:58:37.685Z
Hash: cb54e4608cfb1115882b
Time: 7682ms
chunk {main} main.js, main.js.map (main) 10.7 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 227 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.22 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 15.9 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.29 MB [initial] [rendered]
Enter fullscreen mode Exit fullscreen mode

After opening provided by command output URL, with the browser, you should see:

Browser screenshot

You can find all the code up to this point in a GitHub repository which you can clone:

git clone -b tutorial1_step1 https://github.com/maciejtreder/angular-seo.git angularSeo
cd angularSeo/
npm install
ng serve
Enter fullscreen mode Exit fullscreen mode

Components, routing, and services

Our app isn’t really complicated so far. Let’s add some routes, other components, and a service to it:

ng g c first
ng g c second
ng g c menu
ng g s echo
Enter fullscreen mode Exit fullscreen mode

Ok, we have a lot of files. We will start with src/app/first/first.component.ts; it will be really straightforward. Replace the default contents with the following:

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

@Component({
   template: '<h1>Hello World!</h1>'
})
export class FirstComponent {

}
Enter fullscreen mode Exit fullscreen mode

Edit the second component, src/app/second/second.component.ts, which is a little bit more complex:

import { Component, OnInit } from '@angular/core';
import { EchoService } from '../echo.service';
import { Observable } from 'rxjs';

@Component({
  templateUrl: `second.component.html`,
  styleUrls: ['./second.component.css']
})
export class SecondComponent implements OnInit {
  public response: Observable<any>;

  constructor(private echoService: EchoService) {}

  public ngOnInit(): void {
      this.response = this.echoService.makeCall();
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Your development environment or linter may call out makeCall(). Don’t worry about this: we’ll resolve it below when we create the service.

We introduced few mechanisms here. First is the use of an external template and an external style (templateUrl and stylesUrls). Angular gives us the ability to create HTML templates and CSS stylesheets outside of the component class.

Here is how the template for the second component, src/app/second/second.component.html, should look:

<h1>Second component</h1>
<h2>This component injects EchoService</h2>
Response is: <span>{{response | async | json}}</span>
Enter fullscreen mode Exit fullscreen mode

And the stylesheet, src/app/second/second.component.css, should look like this:

span {   
   color: purple;
   display: block;
   background: #ccc;
   padding: 5px;
}
Enter fullscreen mode Exit fullscreen mode

Another mechanism we introduced in this component is dependency injection. If you take a close look at the constructor in second.component.ts you will see a parameter of type EchoService. Angular will try to initialize and pass an object of the EchoService type to our SecondComponent class when it is initialized. Dependency injection is a technique used to implement an architectural paradigm known as inversion of control (IoC).

We also introduced an Observable type and an async pipe in the template. Are you familiar with Promises? An Observable is one step further. This asynchronous type emits values pushed to it by other functions. You can reuse it as many times as you want, subscribe multiple listeners, and more (map, filter, pass to another observable, etc.) You can read more about it at the RxJS GitHub page.

The async pipe in second.component.html template is a special Angular mechanism to display our variable in the view template only when it is evaluated. In other words, the value pushed to the HTML at runtime is sent by the EchoService observable.

Last, but not least, we implemented the OnInit interface and the ngOnInit lifecycle hook. Interfaces in TypeScript work the same way as interfaces in other languages; if you implement it, you must implement all the methods declared in it. In this particular case, we need to implement the ngOnInit() method. This method is one of the “Angular lifecycle hooks”, methods called automatically by the Angular engine at different stages of view initialization, destruction and other events. According to the Angular documentation:

ngOnInit - Initialize the directive/component after Angular first displays the data-bound properties and sets the directive/component's input properties. Called once, after the first ngOnChanges().

We injected a service into the second component. Now we can create it. Replace the default contents of src/app/echo.service.ts with the following:

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable({
 providedIn: 'root'
})
export class EchoService {
   constructor(private httpClient: HttpClient) {}

   public makeCall(): Observable<any> {
       return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1');
   }
 }
Enter fullscreen mode Exit fullscreen mode

This service contains only one method, which makes a GET request to the https://jsonplaceholder.typicode.com/posts/1 URL.

Ok. That magic looks awesome. But you are probably asking “How does Angular know what to inject? Where to inject it? Where are all those classes initialized?”. And those are great questions. The answer is: NgModule, the entry point of our app. Time to take a look at what is going on inside of it and import one more module which is necessary for EchoService: HttpClientModule (src/app/app.module.ts):

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { FirstComponent } from './first/first.component';
import { SecondComponent } from './second/second.component';
import { MenuComponent } from './menu/menu.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
declarations: [
  AppComponent,
  FirstComponent,
  SecondComponent,
  MenuComponent
],
imports: [
  BrowserModule,
  AppRoutingModule,
  HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

What do we have here …?

  • Imports - links to other NgModules
  • Declarations - a list of components used in the application
  • Bootstrap - the name of the component we want to load as a “main” one
  • Providers - a list of services used across the application

Ok, now it’s time to declare routing. Start by creating a routing module:

ng generate module app-routing --flat --module=app
Enter fullscreen mode Exit fullscreen mode

We can now add our routes to src/app/app-routing.module.ts, export RouterModule from it, and remove redundant code (the declarations array and CommonModule import):

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FirstComponent } from './first/first.component';
import { SecondComponent } from './second/second.component';

@NgModule({
 imports: [
   RouterModule.forRoot([
     { path: '', redirectTo: '/firstComponent', pathMatch: 'full' },
     { path: 'firstComponent', component: FirstComponent },
     { path: 'secondComponent', component: SecondComponent }
   ])
 ],
 exports: [ RouterModule ]
})
export class AppRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

We can add some links in the MenuComponent file, src/app/menu/menu.component.ts:

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

@Component({
  selector: 'app-menu',
  template: `
      <ul>
          <li><a routerLink="firstComponent">First component</a></li>
          <li><a routerLink="secondComponent">Second component</a></li>
      </ul>
  `,
  styles: [`
      :host {margin: 0; padding: 0}
      ul {list-style-type: none; padding: 0;}
      li {display: inline-block;}
      a {
          border: 1px solid #666666;
          background: #aaaaaa; border-radius: 5px;
          box-shadow: 1px 1px 5px black;
          color: white;
          font-weight: bold;
          padding: 5px;
          text-decoration: none;
       }
      li + li a {margin-left: 20px;}
  `]
})
export class MenuComponent {
}
Enter fullscreen mode Exit fullscreen mode

Do you see a connection between routerLink and routes declared in AppModule? Great! This is how we are linking stuff in Angular: routerLink is an Angular built-in directive which takes path as a parameter and matches it with path declared in RouterModule.forRoot(). When there is a match, it loads the given component into a <router-outlet> component, which we are going to add into src/app/app.component.html right now. Replace the code from this file with:

<app-menu></app-menu><router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

Our app is ready. Time to launch it:

ng serve -o
Enter fullscreen mode Exit fullscreen mode

This is what you should see in your browser:

Browser screenshot

When you click the Second component button you’ll see this if everything is working correctly:

Browser screenshot

Find all the code up to this point in this GitHub repository you can clone:

git clone -b tutorial1_step2 https://github.com/maciejtreder/angular-seo.git angularSeo
cd angularSeo/
npm install
ng serve -o
Enter fullscreen mode Exit fullscreen mode

Search-engine Optimization (SEO)

Our app looks ready to deploy. But there are some issues if we "think" like a network crawler.

Run our application:

ng serve
Enter fullscreen mode Exit fullscreen mode

Take a look at our website. You can do this by inspecting the page source or by running the cURL command below (if you have cURL installed):

curl localhost:4200

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularSeo</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root class="content"></app-root>
  <footer>Built with <a href="https://github.com/maciejtreder/ng-toolkit">ng-toolkit</a> by <a href="https://www.maciejtreder.com">maciejtreder.com</a></footer>
<script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body>
</html>
Enter fullscreen mode Exit fullscreen mode

What’s the problem here? How do single page apps work? In fact, they are pure HTML, with tons of JavaScript attached and executed in the user’s browser.

Are crawlers able to do the same? GoogleBot can render JavaScript, but other crawlers (like Facebook, LinkedIn, Twitter, Bing) can't. Also, websites that “look” like static ones, and don’t expect extra resource requirements from the crawler to read them, are positioned higher in search engine ranking because of their better performance.

How could we solve this issue? Super simple! Type following in your command line:

ng add @ng-toolkit/universal
Enter fullscreen mode Exit fullscreen mode

What just happened? @ng-toolkit updated our project with Angular Universal functionality, technology that runs your Angular application on the server. We have a couple of new files created.

Changes have been made to the src/app/app.module.ts file, which was the entry point of our app. What @ng-toolkit did is remove the bootstrap attribute from the @NgModule annotation, removed BrowserModule from imports array and add a couple new there, NgtUniversalModule and CommonModule.

Where our app is bootstrapped now? In fact, it depends. If you are looking for the bootstrap used by the browser, we should navigate to the src/app/app.browser.module.ts, this is where it resides:

import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
   bootstrap: [AppComponent],
   imports: [
       BrowserModule.withServerTransition({appId: 'app-root'}),
       AppModule,
   ]
})
export class AppBrowserModule {}
Enter fullscreen mode Exit fullscreen mode

What @ng-toolkit has also done, is the creation of the src/app/app.server.module.ts. This is an entry point for the code which will be executed on the server side.

Now we can take a look at the project configuration file, angular.json. What we will find there is a new builder added to our project.

(Ellipsis (“...”) in a code block indicates a section redacted for brevity.)

...
"server": {
  "builder": "@angular-devkit/build-angular:server
    "options": {
      "outputPath": "dist/server",
      "main": "src/main.server.ts",
      "tsConfig": "src/tsconfig.server.json"
    }
  }
}
...
Enter fullscreen mode Exit fullscreen mode

As you can see, the entry file for this build is src/main.server.ts, which has been added to our project as well. By looking at this file we can determine the entry point used by the Angular compiler to create the server side build:

import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

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

export {AppServerModule} from './app/app.server.module';
Enter fullscreen mode Exit fullscreen mode

Here it is: src/app/app.server.module.ts, which is the server-side rendering equivalent of src/app/app.browser.module.ts, the module that bootstraps the app for browser rendering.

As you probably noticed, @ng-toolkit also made changes in the package.json file. We have a couple new scripts there:

...
"build:server:prod": "ng run angularSeo:server && webpack --config webpack.server.config.js --progress --colors",
"build:browser:prod": "ng build --prod",
"build:prod": "npm run build:server:prod && npm run build:browser:prod", "server": "node local.js"
...
Enter fullscreen mode Exit fullscreen mode

The two most important are at the end of the list:

  • build:prod, which runs the Angular compiler against the browser and server builds and then creates a server.js file, using the Webpack configuration added by @ng-toolkit.
  • server, which is used to start Node.js with the compiled application.

Give them a try:

npm run build:prod
npm run server
Enter fullscreen mode Exit fullscreen mode

We can try to behave like a network crawler:

curl localhost:8080
Enter fullscreen mode Exit fullscreen mode

And boom! If we inspect the page source we can see the generated on the server side. Look for the links generated for the buttons, shown below:

...
<app-root class="content" _nghost-sc0="" ng-version="6.1.10"><menu _ngcontent-sc0="" _nghost-sc1="">
<ul _ngcontent-sc1="">
<li _ngcontent-sc1=""><a _ngcontent-sc1="" routerlink="firstComponent" href="/firstComponent">First component</a></li>
<li _ngcontent-sc1=""><a _ngcontent-sc1="" routerlink="secondComponent" href="/secondComponent">Second component</a></li>
</ul></menu>
<router-outlet _ngcontent-sc0=""></router-outlet><ng-component><h1>Hello World!</h1></ng-component></app-root>
...
Enter fullscreen mode Exit fullscreen mode

If you don’t see the expected results or encountered an error, you can find all the code up to this point in this GitHub repository, which you can clone:

git clone -b tutorial1_step3 https://github.com/maciejtreder/angular-seo.git angularSeo
cd angularSeo/
npm install
npm run build:prod
npm run server
Enter fullscreen mode Exit fullscreen mode

Deploy!

Awesome! Our app is fully developed and SEO friendly. We are 95% ready for deployment.

We should discuss the remaining 5%.

Usually, we would build our application and publish everything that is under the dist folder to some hosting service (for example, Amazon S3). The “problem” is that we introduced the server-side rendering mechanism, which needs Node.js running on the backend machine. Do we need a cost-consuming EC2 instance running for 24 hours per day?

Nope. We are going to use AWS Lambda, a Function as a Service (FaaS) environment together with Serverless Framework. By using @ng-toolkit again, we will set up basic configuration for the Serverless Framework.

ng add @ng-toolkit/serverless
Enter fullscreen mode Exit fullscreen mode

This command creates the serverless.yml file, which provides configuration for the Serverless Framework, and the lambda.js file, which provides the entry point for the AWS Lambda function. It also makes some minor changes in the framework.

To configure the project to run in your default AWS region, edit the serverless.yml file and replace the value of region: with the name of your default AWS region. For example, replace eu-central-1 with us-east-2.

We also have new scripts in package.json. Make use of one of those:

npm run build:serverless:deploy

...
endpoints:
  ANY - https://5k42947te1.execute-api.eu-central-1.amazonaws.com/production/{proxy+}
  ANY - https://5k42947te1.execute-api.eu-central-1.amazonaws.com/production
functions:
  api: angularseo-production-api
Enter fullscreen mode Exit fullscreen mode

You should be able to navigate to your application at the URL found in the deploy command output, like that shown above. The URL will include your default region if you changed the value in serverless.yml.

When you click the Second component button the output should look like this:

Alt Text

As a last step, check that URL with the curl command or by inspecting the page source:

curl https://5k42947te1.execute-api.eu-central-1.amazonaws.com/production
...
<app-root class="content" _nghost-sc0="" ng-version="6.1.10"><menu _ngcontent-sc0="" _nghost-sc1="">
<ul _ngcontent-sc1="">
<li _ngcontent-sc1=""><a _ngcontent-sc1="" routerlink="firstComponent" href="/production/firstComponent">First component</a></li>
<li _ngcontent-sc1=""><a _ngcontent-sc1="" routerlink="secondComponent" href="/production/secondComponent">Second component</a></li>
</ul>
</menu><router-outlet _ngcontent-sc0=""></router-outlet>
<ng-component><h1>Hello World!</h1></ng-component></app-root>
...
Enter fullscreen mode Exit fullscreen mode

Perfect. Our app is live on the web.

You can find all the code up to this point in a GitHub repository which you can clone:

git clone -b tutorial1_step4 https://github.com/maciejtreder/angular-seo.git angularSeo
cd angularSeo/
npm install
npm run build:serverless:deploy
Enter fullscreen mode Exit fullscreen mode

Summary

Today we have successfully developed and deployed an Angular 7 application on AWS Lambda. You have learned how introduce routing, modules, and services in Angular 7, all of that with server-side rendering for SEO optimization purposes. These techniques enable you to deploy your Angular application on the web in a way that’s accessible to search engines while minimizing your operational costs. With AWS Lambda you pay just for the compute time required to serve your visitor’s requests instead of paying for each minute the server is online.

Resources

GitHub repository: https://github.com/maciejtreder/angular-seo/tree/tutorial1_step4

Also, check out: https://github.com/maciejtreder/ng-toolkit for more Angular and Angular + Serverless Framework features; and my previous article How to Deploy JavaScript & Node.js Applications to AWS Lambda. I'm Maciej Treder, and you can contact me via email at contact@maciejtreder.com or @maciejtreder (GitHub, Twitter, StackOverflow, LinkedIn).

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .