Build your first serverless app with Angular, NestJS and Azure

Yohan Lasorsa - Sep 27 '19 - - Dev Community

Ever wanted to take the plunge and see what it looks like to build a complete serverless app from scratch? Let's just do that!

In this article we will go through all the steps to bootstrap, build and deploy a complete application using the Nitro stack: Angular for the frontend, NestJS for the backend and Azure Serverless platform for deployment.

The app we will build is a simple "random cat fact" that will fetch an interesting cat fact from our API and display it on a web page, like this:

final app screenshot showing a cat fact and a cat emoji

This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles are published every day — that's right, every day — from community members and cloud advocates in the month of September.

Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/.

TL;DR key takeaways

  • If you like Angular, you should take a look at NestJS: it leverages the same concepts, features, and architecture but for Node.js backend development
  • Preparing a NestJS app for serverless deployment with Azure Functions can be done with just nest add @nestjs/azure-func-http and requires no changes to the existing structure
  • Nitro stack (= Angular + NestJS + Azure Serverless) allows you to build full-stack TypeScript apps in a consistent, robust and cost-efficient way

Here is the final project source code on GitHub.

What will you learn here?

In this article, we will:

  • Build a serverless Node.js API from scratch using NestJS
  • Build an Angular app and connect it to your backend for both local and remote development
  • Use Azure Functions to deploy your API in a cost-efficient and scalable way
  • Use Azure Storage static website hosting to deploy your Angular app with ng deploy

Reference links for everything we use

Requirements

An Azure account is also needed to create resources and deploy the application. If you don't have an account, you can create one for free using this link (includes free credits, more than enough to cover usage for this article).

Getting started

Let's create a new folder in which we will put both the frontend and backend code.

$ mkdir catfacts
$ cd catfacts
Enter fullscreen mode Exit fullscreen mode

We will first build the API to get random cat facts, then build the frontend that will use and it and display the fact on a web page.

Building the backend

The backend for our application will be built using NestJS.

If you're not familiar with NestJS, it's a TypeScript Node.js framework that looks a lot like Angular and helps you build enterprise-grade efficient and scalable Node.js applications.

Install NestJS CLI and scaffold new server app

Use the following commands to install the NestJS CLI and create a new server app:

$ npm install -g @nestjs/cli
$ nest new catfacts-server
$ cd catfacts-server
Enter fullscreen mode Exit fullscreen mode

Create the random fact endpoint

Then we use the NestJS CLI again to create a new controller:

$ nest generate controller facts
Enter fullscreen mode Exit fullscreen mode

Open the file src/facts/facts.controller.ts and add this cat facts list after the imports:

// Some cat facts, courtesy of https://catfact.ninja
const catFacts = [
  "Cats have supersonic hearing",
  "On average, cats spend 2/3 of every day sleeping. That means a nine-year-old cat has been awake for only three years of its life.",
  "A cat uses its whiskers for measuring distances. The whiskers of a cat are capable of registering very small changes in air pressure.",
  "A healthy cat has a temperature between 38 and 39 degrees Celcius.",
  "A cat’s jaw can’t move sideways, so a cat can’t chew large chunks of food.","Jaguars are the only big cats that don't roar.",
  "Cats have 'nine lives' thanks to a flexible spine and powerful leg and back muscles",
  "The cat's tail is used to maintain balance.",
  "The technical term for a cat’s hairball is a 'bezoar.'",
  "The first cat show was organized in 1871 in London. Cat shows later became a worldwide craze.",
  "A happy cat holds her tail high and steady.",
  "A cat can jump 5 times as high as it is tall."
];
Enter fullscreen mode Exit fullscreen mode

Then add a new method in the existing FactsController class, like this:

@Controller('facts')
export class FactsController {

  @Get('random')
  getRandomFact(): string {
    return catFacts[Math.floor(Math.random() * catFacts.length)];
  }

}
Enter fullscreen mode Exit fullscreen mode

Now let's break down what we just did:

  • The @Controller() annotation specifies that this class will handle incoming requests and return responses to the client. The optional argument 'facts' used here will be used as the base route prefix for all handlers defined within that class.

  • The @Get() annotation defines a new HTTP GET request handler, creating a new endpoint. The optional argument 'random' will be used as a path for this endpoint.

Combining the controller path prefix with our request handler path, NestJS will create the GET /facts/random HTTP endpoint.

This endpoint will return a status code 200 and the associated response, which in our case is just a string.

You can look at the NestJS controller documentation for the list of all annotations and options that can be used to define endpoints.

Add a global API prefix

A common good practice is to define a global path prefix for all your endpoints, making it easy to later version your API or expose it alongside static assets.

You can do this in NestJS by editing the src/main.ts and call the setGlobalPrefix() method after the app creation:

const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
Enter fullscreen mode Exit fullscreen mode

After this change, our HTTP endpoint will be GET /api/facts/random.

Run the server

Our server is now ready for local testing, you can run it with the command:

$ npm start
Enter fullscreen mode Exit fullscreen mode

After the server is started, you can test if our API is responding properly using curl:

$ curl http://localhost:3000/api/facts/random
Enter fullscreen mode Exit fullscreen mode

You should see a random cat fact in the console each time you invoke this command. You now have a working API! 🎉

Let's keep the server running, open a new terminal and move on to the frontend.

Building the frontend

Time to build our web app to display the cat facts! 🐈

We will use Angular to build the frontend for our application, and explore some of the CLI options to make it easy to work with our server API running locally for development.

Install Angular CLI and scaffold new client app

Let's install the Angular CLI and use it to create the client app with these commands:

# Make sure to go back to your project root first!

$ npm install @angular/cli
$ ng new catfacts-client --defaults
$ cd catfacts-client
Enter fullscreen mode Exit fullscreen mode

Prepare your app config

First, let's add a new apiUrl property to our environment config in src/environments/environment.ts, so we can tell where to consume our API for development (we will take care of the production URL once our server is deployed):

export const environment = {
  production: false,
  apiUrl: '/api'
};
Enter fullscreen mode Exit fullscreen mode

Using just the relative path /api means that we will do our API calls on the same domain as the client, so we have to set up a proxy for Angular CLI dev server to forward these calls to our API server running locally.

To do that, we will create a new file proxy.local.js at the root of our client project:

/*
 * This allows you to proxy HTTP requests like `http.get('/api/stuff')` to another server/port.
 * This is especially useful during development to avoid CORS issues while using a local server.
 * For more details and options see: https://angular.io/guide/build#proxying-to-a-backend-server
 */
module.exports = {
  '/api': {
    target: 'http://localhost:3000',
    secure: false
  }
};
Enter fullscreen mode Exit fullscreen mode

Then we will add a new NPM script to our package.json file to use it:

"scripts": {
  "start:local": "ng serve --proxy-config proxy.local.js",
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now we can run the command npm run start:local, and the Angular CLI will start a dev server with a proxy to consume the API of our running local NestJS backend.

Fetch a random fact and display it

To fetch data from our API we will use Angular built-in HttpClient, so we first have to import the module in src/app/app.module.ts:

import { HttpClientModule } from '@angular/common/http';
Enter fullscreen mode Exit fullscreen mode

Then add the module to your AppModule imports declaration:

imports: [
  BrowserModule,
  HttpClientModule
],
Enter fullscreen mode Exit fullscreen mode

After that, open src/app/app.component.ts and change it to make the API call:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  fact$: Observable<string>;

  constructor(http: HttpClient) {
    this.fact$ = http.get(
      `${environment.apiUrl}/facts/random`,
      { responseType: 'text' }
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

The important part is this code:

this.fact$ = http.get(
  `${environment.apiUrl}/facts/random`,
  { responseType: 'text' }
);
Enter fullscreen mode Exit fullscreen mode

Here we use the Angular HTTP client injected in the constructor to make a GET HTTP request to our cat facts API, using the apiUrl prefix from our environment config. Since our request result is just plain text and not a JSON object, we also have explicitly set the response type to 'text' so Angular won't try to parse it.

Note that this line will create the fact$ observable, but no actual HTTP call will be made until we subscribe to that observable!
For that we will use the AsyncPipe in our template which will automatically subscribe/unsubscribe from this observable.

Now edit src/app/app.component.html and replace all the boilerplate HTML to display our cat fact instead:

<article>
  <p>{{ fact$ | async }}</p>
  <p>😸</p>
</article>
Enter fullscreen mode Exit fullscreen mode

We used the async pipe here to do all the magic: it will subscribe to the fact$ observable, wait until the data is received to display it, and take care of the cleaning by unsubscribing when the component is destroyed.

Test the result locally

Make sure your API server is still running, and run the dev server for your frontend with the command npm run start:local if it's not done already.

Open the URL http://localhost:4200 in your browser, and you should see something like this:

current app screenshot showing a cat fact with a cat emoji, without styling

Your app is working (locally)!

(Optional) paint job

Let's add a bit of CSS to make your app nicer! 🦄

Open src/styles.css to add a micro CSS reset and a background gradient:

html, body {
  margin: 0;
  height: 100%;
  background: linear-gradient(120deg, #f6d365, #fda085);
}
Enter fullscreen mode Exit fullscreen mode

Now open src/app/app.component.css to style our cat fact, by changing the font and adding some margins:

@import url('https://fonts.googleapis.com/css?family=Caveat&display=swap');

p {
  font-family: 'Caveat', cursive;
  font-size: 2.5rem;
  margin: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

Tip: you can go to https://fonts.google.com and choose whatever font you like the best!

Let's also center the article block vertically and the text horizontally:

article {
  text-align: center;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Now the result should be a bit better, like this:

styled app screenshot showing a cat fact and a cat emoji

Deploying your app

At this point you should have your complete app running locally, now it's time to deploy it to the cloud️.

Go serverless

Why serverless? Why not deploy our server on a container or an Azure App Service for instance? There are 2 main reasons for that:

  • You only pay for what you use, not for the resource allocation (and it's dead cheap!)
  • It scales automatically without anything to setup

First, let's update our NestJS into a serverless app so we can deploy it to Azure Functions.

Open a terminal to your server folder, and run this command:

$ nest add @nestjs/azure-func-http
Enter fullscreen mode Exit fullscreen mode

And... That's it. Really.

grandma looking surprised

Thanks to the included schematics, your server code is now ready to be deployed on Azure Functions! And the best part is that nothing was changed in your code, it can still run locally like previously.

If you take a closer look, here is what has been added:

  • main: a folder containing the Azure Functions trigger config and entry point
  • src/main.azure.ts: an alternative entry point for your server app that will be used only on Azure Functions (thus leaving the regular entry point intact).
  • Some config files in the project's root, we do not need to care about them for now.
  • A new start:azure NPM script in your package.json file, allowing to run your API locally but with the Azure Functions simulator this time.

To be able to use the command npm run start:azure, you must install the Azure Functions Core Tools. It will install the func CLI that you can use to test your functions and deploy them to Azure.

Once you have the func CLI installed, let's test our API running on a function:

$ npm run start:azure
Enter fullscreen mode Exit fullscreen mode

If everything is working well, at some point you should see this log in the console:

Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.

Http Functions:

        main:  http://localhost:7071/api/{*segments}
Enter fullscreen mode Exit fullscreen mode

That means that your API should be working on port 7071, let's test it again using curl:

$ curl http://localhost:7071/api/facts/random
Enter fullscreen mode Exit fullscreen mode

Create the resources and deploy to Azure Functions

Now that our API is ready to run on Functions, let's deploy it to the real thing!

First, we have to create some Azure resources. For that we will use the Azure CLI commands:

# Create a new resource group
$ az group create --name catfacts --location northeurope

# Create the storage account
# This name must be globally unique, so change it with your own
$ az storage account create --name catfacts \
                            --resource-group catfacts \
                            --kind StorageV2

# Create the function app
# This name must be globally unique, so change it with your own
$ az functionapp create --name catfacts-api \
                        --resource-group catfacts \
                        --consumption-plan-location northeurope \
                        --storage-account catfacts
Enter fullscreen mode Exit fullscreen mode

Now that we have the resources created on Azure, we can use the func CLI to deploy our API:

# Build your app
$ npm run build

# Clean up node_modules to keep only production dependencies
$ npm prune --production

# Create an archive from your local files and publish it
# Don't forget to change the name with the one you used previously
$ func azure functionapp publish catfacts-api
Enter fullscreen mode Exit fullscreen mode

After publishing, you should see in the console the URL you can use to invoke the function, like this:

Functions in catfacts-api:
    main - [httpTrigger]
        Invoke url: https://catfacts-api.azurewebsites.net/api/{*segments}
Enter fullscreen mode Exit fullscreen mode

We can invoke our trusty curl command again to check the deployment using the previous URL:

curl https://catfacts-api.azurewebsites.net/api/facts/random
Enter fullscreen mode Exit fullscreen mode

Server deployment, done. ✔️

Prepare the frontend for production

Now that our server is deployed, we need to add the apiUrl property in our production environment config in src/environments/environment.prod.ts, like we did previously for development:

export const environment = {
  production: true,
  // Use the function app URL you got from the previous step
  apiUrl: 'https://catfacts-api.azurewebsites.net/api'
};
Enter fullscreen mode Exit fullscreen mode

Deploy the frontend

The final step is to deploy our Angular app to Azure. For that we will use the Angular CLI with the @azure/ng-deploy package:

# Make sure to use the storage account name created previously
$ ng add @azure/ng-deploy --resourceGroup catfacts --account catfacts
Enter fullscreen mode Exit fullscreen mode

You will be prompted to log in to your Azure account, then you will need to choose the same subscription used for creating the resources previously.

As we want to reuse the same storage account we used for the backend, we have to enable static website hosting on it with this command:

# Don't forget to change the account name with the one you used previously
$ az storage blob service-properties update \
    --account-name catfacts \
    --static-website \
    --404-document index.html \
    --index-document index.html
Enter fullscreen mode Exit fullscreen mode

After that you can directly deploy your app from the Angular CLI using the command:

$ ng deploy
Enter fullscreen mode Exit fullscreen mode

Your app will be built for production and then deployed to your Azure Storage website container.

Once the deployment is finished, open the URL returned by the tool that looks like https://catfacts.z16.web.core.windows.net/ and voilà.

Wait a minute, it's not working! There's no cat fact displayed? 😱
If you open the console, you should see an error like this:

error in chrome console: access to XMLHttpRequest at URL from origin URL has been blocked by CORS policy

Fix CORS issue

This error occurs because browsers block HTTP requests from scripts to web domains different than the one of the current web page to improve security.

To bypass this restriction, your web server must define specific HTTP headers to allow it. This mechanism is called Cross-Origin Resource Sharing (CORS).

CORS is already enabled by default on Azure Functions, but you must add your website domain to the list of allowed origins using this command:

# Don't forget to change the name and URL with your own
$ az functionapp cors add \
    --name catfacts-api \
    --resource-group catfacts \
    --allowed-origins https://catfacts.z16.web.core.windows.net
Enter fullscreen mode Exit fullscreen mode

If you refresh your web page, you should now see the cat fact properly.

Wrap it up

Congratulations, you have just finished deploying your first complete serverless app! ⚡️💪

Let's quickly review what we just did:

  • We bootstrapped a new NestJS server, created an API and prepared for serverless deployment.
  • We created a new Angular web app to consume your API and display the data, with different environment configurations for development and production.
  • We provisioned Azure resources and deployed our app on it.

This was a bit lengthy, but now you have a complete end-to-end experience from bootstrapping to release a full-stack serverless app 🚀.

The full source code for the app we just built can be found here on GithHub.

Share your feedback and experience in comments!

Going further

We only scratched the surface here, but you have already seen how quickly and easily you can create and deploy a complete application with Angular, NestJS and Azure.

Our current app is very basic for now, but it doesn't get much harder to extend it by adding CRUD operations on a database, file uploads and so on.

Here are some article recommendations if you want to dig in further:

For a more complete example app that also includes file upload and database connection using Azure Storage, you can take a look at the Nitro Cats demo source code.


Follow me on Twitter, I would be happy to discuss and take your suggestions!

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