Building a Serverless Mobile App Backend with AWS Lambd

Author: Zen and the Art of Computer Programming

1. Introduction

The term “Serverless” has attracted more and more attention in recent years. It allows developers to only focus on business logic development without having to worry about a series of cumbersome processes such as server operation and maintenance, resource configuration, and application deployment.

As a technician, I believe that any technological innovation is inseparable from the understanding and grasp of current business. For mobile terminal research and development, how to use AWS services to build a low-cost, high-efficiency, and highly scalable backend is an important topic.

In the past period of time, I have been exploring the mobile backend technology stack, including services on AWS such as Lambda, API Gateway, DynamoDB, etc., as well as similar domestic products, such as Microsoft’s Mobile Apps backend as a service (MBaaS) , LeanCloud, etc.

I’ve long been aware of AWS Mobile Hub, which is designed to quickly create mobile application backends. However, although it provides a convenient interface to help create various services required for mobile applications, its functionality is not comprehensive enough. For example, it has no support for a database access layer. Therefore, when I need to build a mobile application backend myself, I will refer to other solutions, including Leancloud, OneAPM, Tencent Cloud SCF, etc. However, these platforms often charge fees and are not versatile enough.

Based on this, I hope to use this article to share how I use AWS services to build a low-cost, high-efficiency, and highly scalable mobile application backend.

2.Basic concepts and terminology

2.1 Service

2.1.1 AWS Lambda

AWS Lambda is a serverless computing service that provides an environment for running functions. You can upload a code package or write the code directly, and then specify the execution time, memory size, and disk space, and AWS will automatically allocate the running environment and resources. Since it is serverless computing, users only need to focus on their own business logic and do not need to care about server resource allocation, network connections, load balancing, etc. Its architecture is shown in the figure below:

2.1.2 Amazon API Gateway

Amazon API Gateway is a RESTful API gateway service that hosts web services. It helps you define RESTful APIs, map them to backend services (such as Lambda), and add security controls (such as authentication and authorization). You can call backend services through this gateway, and it can also publish your API to the public Internet for use by third-party developers. Its architecture is shown in the figure below:

2.1.3 Amazon DynamoDB

Amazon DynamoDB is a fast, highly scalable NoSQL database service. It allows you to store and retrieve structured and unstructured data with elasticity, high availability, and scalability. Its architecture is shown in the figure below:

2.2 Tools

2.2.1 AWS CLI

AWS CLI is a command line tool for managing AWS services. You can use it to complete many tasks, such as creating, updating and deleting EC2 instances, IAM policies, S3 buckets, etc. Please refer to the official documentation for installation methods.

2.2.2 AWS SDKs

AWS SDKs are a collection of APIs used by developers to interact with AWS services. You can use them to build your own applications or scripts to implement various functions, such as sending emails, querying cloud monitoring information, uploading files, etc. Please refer to the official documentation for details.

3. Core algorithm principles and specific operating steps

3.1 Create backend service

The first step is to create a new project and initialize the AWS environment. If you are not familiar with the AWS CLI, you can read the relevant documentation first and configure the appropriate credentials.

  1. Initialize project
mkdir mobile-app-backend & amp; & amp; cd mobile-app-backend
npm init -y
  1. Install dependent libraries
npm install --save aws-sdk
  1. Configure AWS credentials
export AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY>
export AWS_SECRET_ACCESS_KEY=<YOUR_SECRET_KEY>
export AWS_REGION=<YOUR_REGION>
  1. Create configuration file

Create a file called serverless.yml with the following content:

service: mobile-app-backend # Project name

provider:
  name: aws # Use AWS services
  runtime: nodejs10.x # Specify the runtime environment as Nodejs

  stage: dev # define environment
  region: us-east-1 # Set region

  environment:
    TABLE_NAME: todos #Set environment variables

  iamRoleStatements: # Set permissions for Lamdba roles
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.TABLE_NAME}"

functions: # Function configuration
  createTodo: # Function to create TODO items
    handler: functions/createTodo.handler # Function entry
    events:
      - http: POST /todos # HTTP interface configuration
      -apigw:
          method: any # Allow any HTTP method
          path: /todos # Set path

  getTodos: # Function to get all TODO items
    handler: functions/getTodos.handler # Function entry
    events:
      - http: GET /todos # HTTP interface configuration
      -apigw:
          method: any # Allow any HTTP method
          path: /todos # Set path

  deleteTodo: # Function to delete a single TODO item
    handler: functions/deleteTodo.handler # Function entry
    events:
      - http: DELETE /todos/{todoId} # Path parameter binding
      -apigw:
          method: delete # Only DELETE requests are allowed
          requestTemplates:
            application/json: '{"todoId": $input.params("todoId")}'
          apiKeyRequired: false # API Key verification is not required

  updateTodo: # Function to update a single TODO item
    handler: functions/updateTodo.handler # Function entry
    events:
      - http: PUT /todos/{todoId} # Path parameter binding
      -apigw:
          method: put # Only PUT requests are allowed
          requestTemplates:
            application/json: |-
              {
                "description": "$input.body('$.description')",
                "done": "$input.body('$.done')"
              }
          integration: lambda # Use Lambda as Integration
          uri: arn:aws:apigateway:${self:provider.region}:lambda:path/2015-03-31/functions/${self:service}-${sls:stage}-createTodo/invocations # Set Lambda address

resources: # Resource configuration
  Resources:
    TodosTable: # Configuration of TODO item table
      Type: 'AWS::DynamoDB::Table' # The type is DynamoDB table
      Properties: #Table properties
        TableName: ${self:provider.environment.TABLE_NAME} #Set the table name
        AttributeDefinitions: # Define table fields
          - AttributeName: userId #User ID field
            AttributeType: S #The data type is string
          - AttributeName: todoId # TODO item ID field
            AttributeType: N #The data type is number
        KeySchema: # Primary key index
          - AttributeName: userId
            KeyType: HASH # as hash key
          - AttributeName: todoId
            KeyType: RANGE # as range key
        ProvisionedThroughput: # Set throughput
          ReadCapacityUnits: 1 # Read capacity per second is 1
          WriteCapacityUnits: 1 # The write capacity per second is 1

The main services and tools involved in the above configuration are as follows:

Services and Tools Description
AWS Lambda Serverless computing service for hosting functions
AWS API Gateway RESTful API gateway, used to define API
AWS DynamoDB NoSQL database service for storing data
AWS CLI Command line tool for managing AWS services
AWS SDKs SDKs, used to interact with AWS services

3.2 Data Model Design

In order to store and manage TODO items, we need to design a suitable data model. Here’s a simple example:

{
  id:'string', // unique identifier
  description:'string', // description information
  done: boolean // Whether it has been completed
}

The two fields id and userId are composite primary keys and therefore cannot be repeated. Additionally, we can use DynamoDB’s query and scan operations to obtain and search TODO items.

3.3 Create function

Next, we need to create some functions for handling TODO items. The specific implementation of each function is given here.

3.3.1 Create TODO items

const uuidv4 = require('uuid').v4;

module.exports.handler = async function(event, context, callback) {
  const body = JSON.parse(event.body);
  if (!body ||!body.description) {
    return {statusCode: 400};
  }

  const timestamp = new Date().toISOString();
  const todo = {
    id: `t_${timestamp}_${Math.floor(Math.random() * 100)}`, // Generate unique identifier
    userId: event.requestContext.identity.cognitoIdentityId, // Get the user ID from the request context
    createdAt: timestamp,
    updatedAt: timestamp,
   ...body
  };

  try {
    await this.ddb.put({
      TableName: process.env.TABLE_NAME,
      Item: todo
    }).promise();

    return {
      statusCode: 201,
      headers: {'Access-Control-Allow-Origin': '*'},
      body: JSON.stringify(todo),
    };
  } catch (err) {
    console.error(err);
    return {statusCode: 500};
  }
};

First, we get the JSON data in the HTTP request body from the event object. If necessary parameters are missing, error response code 400 is returned. Otherwise, generate a UUID as the id value of the TODO item, and set the user ID, creation time, update time, and description information in the request body.

Next, we try to insert TODO items into DynamoDB. If it fails, the error log is printed and error response code 500 is returned. When successful, status code 201 is returned, cross-domain access is allowed, and the JSON representation of the TODO item is returned.

3.3.2 Get all TODO items

module.exports.handler = async function(event, context, callback) {
  let responseBody = '';

  try {
    const result = await this.ddb.scan({
      TableName: process.env.TABLE_NAME,
      FilterExpression: `#userId = :userId`,
      ExpressionAttributeNames: {"#userId": "userId"},
      ExpressionAttributeValues: {":userId": event.requestContext.identity.cognitoIdentityId},
      ProjectionExpression: '#id, #desc, #createdAt, #updatedAt, #done',
      ExpressionAttributeNames: {
        "#id": "id",
        "#desc": "description",
        "#createdAt": "createdAt",
        "#updatedAt": "updatedAt",
        "#done": "done"
      },
      Limit: 100 // Return up to 100 pieces of data
    }).promise();

    for (let i = 0; i < result.Items.length; i + + ) {
      responseBody + = `${result.Items[i].id}\t${result.Items[i].description}\t${result.Items[i].createdAt}\t${result. Items[i].updatedAt}\t${result.Items[i].done? 'Yes' : 'No'}\r\\
`;
    }

    return {
      statusCode: 200,
      headers: {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*'},
      body: responseBody
    };
  } catch (err) {
    console.error(err);
    return {statusCode: 500};
  }
};

First, we define a variable responseBody to save the result data. We then try to scan all TODO items from DynamoDB and filter out items that the user is not their own. In order to improve query speed, we only select necessary fields. If it fails, the error log is printed and error response code 500 is returned.

If the query is successful, it loops through all items, assembles them into a string similar to a csv format, and returns status code 200, allowing cross-domain access, and this string.

3.3.3 Delete a single TODO item

module.exports.handler = async function(event, context, callback) {
  const params = {
    TableName: process.env.TABLE_NAME,
    Key: {
      userId: event.requestContext.identity.cognitoIdentityId,
      todoId: parseInt(event.pathParameters.todoId)
    }
  };

  try {
    await this.ddb.delete(params).promise();

    return {
      statusCode: 204,
      headers: {'Access-Control-Allow-Origin': '*'}
    };
  } catch (err) {
    console.error(err);
    return {statusCode: 500};
  }
};

First, we parse out the todoId based on the request path parameters. After that, we construct a Key object containing the user ID and TODO item ID.

Next, we try to delete the specified TODO item from DynamoDB. If it fails, the error log is printed and error response code 500 is returned. When successful, status code 204 is returned, allowing cross-domain access.

3.3.4 Update a single TODO item

module.exports.handler = async function(event, context, callback) {
  const body = JSON.parse(event.body);
  if ((!body.description || typeof body.description!=='string') ||
      (!body.done || typeof body.done!== 'boolean')) {
    return {statusCode: 400};
  }

  const timestamp = new Date().toISOString();
  const params = {
    TableName: process.env.TABLE_NAME,
    Key: {
      userId: event.requestContext.identity.cognitoIdentityId,
      todoId: parseInt(event.pathParameters.todoId)
    },
    UpdateExpression: `SET description=:description, done=:done, updatedAt=:updatedAt`,
    ExpressionAttributeValues: {
      ':description': body.description,
      ':done': body.done,
      ':updatedAt': timestamp
    }
  };

  try {
    const data = await this.ddb.update(params).promise();

    if (!data.Attributes) {
      return {statusCode: 404};
    }

    return {
      statusCode: 200,
      headers: {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
      body: JSON.stringify(data.Attributes),
    };
  } catch (err) {
    console.error(err);
    return {statusCode: 500};
  }
};

First, we check whether the request body contains the correct description and done fields. If the requirements are not met, error response code 400 is returned.

Next, we construct an UpdateExpression object to modify the description information and completion status of the TODO item. Finally, we try to update the specified TODO item. If the specified TODO item cannot be found, error response code 404 is returned; if the update is successful, status code 200 is returned, cross-domain access is allowed, and the complete attributes of the TODO item are returned.