Part two: Serverless Real-Time ReactJS App—AWS IoT MQTT

June 17, 2019

This article is part of a series:

GitHub Repository: https://github.com/serverless-guru/real-time-weather

In part one, we covered how to hook up AWS IoT MQTT with a ReactJS frontend. Giving our ReactJS application real-time communication where every device will instantly* be updated when a new message is published to our MQTT topic.

In part two, we will be extending the application to include the Serverless Framework and do the following:

  • Create a basic Serverless Framework project
  • Dive into best practices with Serverless Framework
  • Add AWS resources
  • Deploy to AWS
  • Test the deployed AWS backend

Let’s get into it!

Create a basic Serverless Framework project

If you’re not familiar with the Serverless Framework and/or do not have an AWS account set up already. Please go through our Introduction to Serverless Framework article or optionally visit our training site and take the Serverless Introduction course.

Create a new project

We will create a new project using a Serverless Framework template which automatically sets up a NodeJS Lambda function ready to deploy to AWS. Since we already created a project last time, just make sure you’re in the project directory.

  
  sls create -t aws-nodejs -p backend -n real-time-weather
  

This command will create a new Serverless project using aws-nodejs template giving us some boilerplate in a folder called backend with a service name equal to real-time-weather 🔥 🔥

First off let’s crack open our favorite text editor and then I’ll show you how to copy/paste like a Senior Cloud Developer!

  
  $real-time-weather: code .
  

Update the provider section (add command line flags)

We will want to be able to pass in a dynamic stage variable, a dynamic region variable, and a dynamic profile variable (for AWS named profiles).

These variables will help ensure:

  • the AWS resource names can be different (multi-stage deployments)
  • the region can be different (multi-region deployments)
  • the AWS account can be different (multi-account deployments)

Let’s copy the following code into our serverless.yml file:

  
  provider:
    name: aws
    runtime: nodejs12.x
    stage: ${opt:stage, "dev"}
    region: ${opt:region, "us-west-2"}
    profile: ${opt:profile, "default"}
  

This will add the following things:

  • a provider of aws
  • a runtime of nodejs12.x
  • an optional stage flag with a default stage of dev
  • an optional region flag with a default region of us-west-2
  • an option profile flag with a default profile of default

To use these flags we will run the following command:

  
  serverless deploy --stage <> --region <> --profile <> -v
  

With the resources/provider.yml isolated, let’s now update the serverless.yml to reference our separate file.

  
  provider: ${file(resources/provider.yml)}
  

Now when we go to make the deployment the provider property will be populated by the resources/provider.yml file.

Create custom variables

Let’s create a custom.yml file under our resources folder at resources/custom.yml. This file will hold all of our custom variables which we can centralize in one place instead of hard-coding in multiple places.

  
  tableName: ${self:service}-${self:provider.stage}-data
  

The key things to pay attention too:

  • ${self:service} is equal to real-time-weather
  • ${self:provider.stage} is equal to dev or the result of --stage <>

With the resources/custom.yml isolated, let’s now update the serverless.yml to reference our separate file.

  
  custom: ${file(resources/custom.yml)}
  

Now when we go to make a deployment the custom property will be populated by the resources/custom.yml file as if it was written inline.

Add AWS resources

Now that we have a serverless project. Let’s start sprinkling ✨ in our AWS resources. We will add the following:

Add DynamoDB

Now let’s create a general.yml file at resources/general.yml and copy the following:

  
  WeatherTable:
    Type: AWS::DynamoDB::Table
    Properties:
      KeySchema:
        - AttributeName: zip
          KeyType: HASH
      AttributeDefinitions:
        - AttributeName: zip
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      TableName: ${self:custom.tableName}
  

This will create a DynamoDB table which has the following settings:

  • a primary key of zip which will be a string
  • a billing mode set to pay per request (e.g. no traffic = no cost)

Let’s update the serverless.yml file to include a reference to our resources/general.yml file.

  
  resources:
    Resources: ${file(resources/general.yml)}
  

Add the API Lambda function

Now let’s create a functions.yml file at resources/functions.yml and copy the following:

  
  api:
    handler: api.handler
    events:
      - http:
          path: /api/weather
          method: GET
          cors: true
    environment:
      TABLE_NAME:
        Ref: WeatherTable
  

Here we are saying a few things:

  • connect our Lambda to the handler() function inside the api.js file
  • create an API at /api/weather which can handle GET requests
  • set cors: true to allow two-way API communication
  • pass an environment variable called TABLE_NAME to our Lambda function
  • reference the DynamoDB table name dynamically via Ref: WeatherTable

We then need to update the serverless.yml to utilize our resources/functions.yml file.

  
  functions: ${file(resources/functions.yml)}
  

Add AWS IAM permissions

Our Lambda function needs to have the proper AWS IAM permissions to access DynamoDB and retrieve weather data.

Let’s add a section to help with this under the provider property in our serverless.yml file.

  
  provider:
    ...
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:GetItem
        Resource:
          Fn::GetAtt:
            - WeatherTable
            - Arn
  

This will do the following:

  • give our Lambda function access to do dynamodb:GetItem
  • specify we can only dynamodb:GetItem on the WeatherTable

Review progress

Perfect, let’s review the serverless.yml file so far.

  
  service: real-time-weather
  provider:
    name: aws
    runtime: nodejs12.x
    stage: ${opt:stage, "dev"}
    region: ${opt:region, "us-west-2"}
    profile: ${opt:profile, "default"}
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:GetItem
        Resource:
          Fn::GetAtt:
            - WeatherTable
            - Arn
  custom: ${file(resources/custom.yml)}
  functions: ${file(resources/functions.yml)}
  resources:
    Resources: ${file(resources/general.yml)}
  

Above, we’ve dramatically reduced the total lines in our serverless.yml file and have chosen to isolate different components of our Serverless backend to separate files. This level of abstraction is particularly useful when you start scaling your projects up.

When your projects grow you may have 3+ DynamoDB tables, a whole slew of supporting AWS services, and 10+ Lambda functions which all need custom variables. As you can imagine this can become hard to manage if it was all pushed into the serverless.yml file.

Therefore we break it up under the resources folder and keep everything nice and neat. That’s called a solid foundation and it’s worth the extra time to set up for every Serverless project you build.

Customize the API Lambda function

First, let’s change the handler.js file to api.js. Then we can paste in the following code:

  
  'use strict';
  const AWS = require('aws-sdk');
  const api = {};
  const tableName = process.env.TABLE_NAME;
  api.handler = async (event, context) => {
      console.log('event', event);
      let response = {};
      try {
          let data = {};
          let path = event.path;
          if(path.includes('weather')) {
              data = await api.handleWeather(event);
          }
          response.statusCode = 200;
          response.body = JSON.stringify(data);
      } catch (error) {
          response.statusCode = 500;
          response.body = JSON.stringify(error);
      }
      response.headers = { 'Access-Control-Allow-Origin': '*' };
      console.log('response: ', response);
      return response;
  };
  api.handleWeather = async (event) => {
      if (event.httpMethod === "GET") {
          return await api.getCurrentWeatherData({ zip: event.queryStringParameters.zipCode });
      } else {
          throw new Error(`event method not known ${event.httpMethod}`);
      }
  };
  api.getCurrentWeatherData = (key) => {
      return new Promise((resolve, reject) => {
          let documentClient = new AWS.DynamoDB.DocumentClient();
  let params = {
              TableName: tableName,
              Key: key
          };

          documentClient.get(params, (err, data) => {
              if (err) reject(err);
              else resolve(data);
          });
      });
  };
  module.exports = api;
  

This code will do a few things:

  • import aws-sdk allowing us to connect to DynamoDB
  • reference an environment variable via process.env.TABLE_NAME
  • add a handleWeather() function to manage all API request to /weather
  • add a getCurrentWeatherData() function to request the weather by zip

From here we should have all we need to fetch data via zip (e.g. 97205) and get a response from the DynamoDB weather table with the current weather in our zip.

Deploy the backend to AWS

With all of our automation created, thanks to the power of the Serverless Framework. Let’s try deploying our Serverless stack to AWS.

  
  sls --stage test --region us-west-2 --profile default -v
  

This will deploy our backend to thetest stage and the us-west-2 region with an AWS named profile of default.

Load some data into DynamoDB

Currently, we only have a GET API endpoint so we will manually add data into DynamoDB then try to pull that data out.

Image for post

Test the AWS backend

Once we have successfully deployed the backend, we can test the API via Postman by making a request with a queryStringParmeter of ?zipCode=97205.

Image for post

Awesome, we’ve got our data coming back from DynamoDB and we have confirmed that our automation fully works to spin up our entire backend.

Tear down

If you would like to stop here, we can tear down the Serverless backend by running the following command.

  
  sls remove --stage test --region us-west-2 --profile default -v
  

This will do the following:

  • delete our Lambda function
  • delete our API Gateway endpoint at /api/weather
  • delete our DynamoDB table
  • delete our AWS IAM policies

Next time, we go even deeper

If you enjoyed this tutorial, please stay tuned for part three which will go into creating a Lambda function which is triggered on a schedule and will auto-post messages out to our AWS MQTT devices.

Allowing our users to get real-time weather updates automatically!

Access free book

The dream team

At Serverless Guru, we're a collective of proactive solution finders. We prioritize genuineness, forward-thinking vision, and above all, we commit to diligently serving our members each and every day.

See open positions

Looking for skilled architects & developers?

Join businesses around the globe that trust our services. Let's start your serverless journey. Get in touch today!
Ryan Jones
Founder
Speak to a Guru
Edu Marcos
Chief Technology Officer
Speak to a Guru
Mason Toberny
Head of Enterprise Accounts
Speak to a Guru

Join the Community

Gather, share, and learn about AWS and serverless with enthusiasts worldwide in our open and free community.