Image of Deploy your First Serverless App Using AWS CDK

ADVERTISEMENT

Table of Contents

Introduction

Serverless architecture has become increasingly popular in recent years due to its ability to reduce operational costs and improve scalability. AWS Lambda, API Gateway, and DynamoDB are three powerful tools provided by Amazon Web Services (AWS) that can be used to create a serverless todo list application. In this article, we will be deploying this application using the AWS Cloud Development Kit (CDK).

Using the AWS CDK, we can easily deploy and manage our serverless todo list application. CDK is a software development framework that provides high-level abstractions for AWS services. This allows developers to use familiar programming languages and tools to define their cloud infrastructure.

This article will focus on creating a simple todo list application, but the approach we discuss can be generalized to nearly any application which implements an API and/or persistent storage. This framework is highly flexible and can be used as a subcomponent of various architectures.

Serverless App Data Model

Since we are creating a todo list application, we will create a data model which represents individual data points (tasks) in our todo list. This task data model represents the repeatable structure for each todo list task that we will store in our DynamoDB database. Our tasks will have two fields: an identifier (task_id) and a title for the task’s content (task_title).

{
	"id" : String (primary key),
	"task_title" : String
}

Each item that we store in DynamoDB contains a “primary key,” or a designated index which we can query for when we want to find an item in the database. In this case, our primary key is id, as we can look for items based on this UUID (universally unique identifier). For more information on DynamoDB or setting up your first table, see our article on How to Create a DynamoDB Table on AWS.

For this application, we will assume that the user application/API call supplies the UUID, but with some minor modifications it is possible to do this server-side through the Lambda function.

Set up basic AWS CDK Infrastructure

To get started, we will start by setting up the skeleton for our infrastructure using CDK. First, let’s create a new CDK application.

$ mkdir todolist-app
$ cd todolist-app
$ cdk init app --language typescript

The todolist-app-stack.ts file is the main entry point for the AWS CDK application. When you run the cdk init command, the following file structure will be created:

todolist-app
- README.md
- bin
-- todolist-app.ts
- lib
-- todolist-app-stack.ts
- package.json
- tsconfig.json
- webpack.config.js

The todolist-app-stack.ts file will be located in the lib directory and will contain the following initial code:

import * as cdk from 'aws-cdk-lib';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';

export class TodolistAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
  }
}

The TodolistAppStack class extends the Stack class provided by the aws-cdk-lib package. This class will be used to define the AWS resources that make up your todo list application. The Constructor function is used to initialize the stack and will be where you will define the resources that make up your application. The scope and id parameters are required by the Stack class, while the props parameter is optional and can be used to specify additional properties for the stack.

Setting Up AWS API Gateway

The first step for creating our todo list application is to build an API. We can build this API using the REST API framework provided through API Gateway. We will use the RestApiclass provided by the aws-cdk-lib/aws-apigateway package. Here is an example of how you can define the API and its resources in the TodolistAppStackclass.

import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';

export class TodolistAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Define the API Gateway construct
    const api = new apigateway.RestApi(this, 'TodoListAPI', {
      restApiName: 'TodoListAPI',
    });

    // Define the "get-tasks" resource
    const getTasks = api.root.addResource('get-tasks');
    const getTasksMethod = getTasks.addMethod('GET');

    // Define the "save-task" resource
    const saveTask = api.root.addResource('save-task');
    const saveTaskMethod = saveTask.addMethod('POST');

    // Define the "delete-task" resource
    const deleteTask = api.root.addResource('delete-task');
    const deleteTaskMethod = deleteTask.addMethod('DELETE');
  }
}

In this code, we first create an instance of the RestApi class and give it a unique ID and a name.

Next, we define the three resources for our API: "get-tasks", "save-task", and "delete-task". For each resource, we create an instance of the addResource method and give it a unique ID. We then specify the HTTP method for each resource using the addMethod method. In this case, we are using the GET, POST, and DELETE methods for the "get-tasks", "save-task", and "delete-task" resources, respectively.

Note that this code is just an example and does not include all the necessary code to fully define the API and its resources. You will need to add additional code to define the integration with the Lambda functions and DynamoDB tables that will be used to implement the API's functionality.

How do you use API gateway and Lambda?

To add a single AWS Lambda construct to handle all three API resources, you can use the Function class provided by the aws-cdk-lib/aws-lambda package. Here is an example of how you can define the Lambda function and its integrations with the API in the TodolistAppStack class:

import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';

export class TodolistAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Define the API Gateway construct
    const api = new apigateway.RestApi(this, 'TodoListAPI', {
      restApiName: 'TodoListAPI',
    });

    // Define the "get-tasks" resource
    const getTasks = api.root.addResource('get-tasks');
    const getTasksMethod = getTasks.addMethod('GET');

    // Define the "save-task" resource
    const saveTask = api.root.addResource('save-task');
    const saveTaskMethod = saveTask.addMethod('POST');

    // Define the "delete-task" resource
    const deleteTask = api.root.addResource('delete-task');
    const deleteTaskMethod = deleteTask.addMethod('DELETE');

    // Define the Lambda function
    const todoListLambda = new lambda.Function(this, 'TodoListLambda', {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset('lambda/todolist'),
      handler: 'index.handler',
    });

    // Add Lambda integrations for the API methods
    getTasksMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
    saveTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
    deleteTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
  }
}

In this code, we first create an instance of the Function class and give it a unique ID. We then specify the runtime, code, and handler for the function. In this case, we are using Node.js 12.x as the runtime, and we are loading the code for the function from the lambda/todolist directory.

Next, we add Lambda integrations for the API methods using the addIntegration method. In this case, we are using the same Lambda function for all three methods by passing the todoListLambda

Adding a DynamoDB Table for Persistent Storage

To add a DynamoDB table for persistent data storage, you can use the Table class provided by the aws-cdk-lib/aws-dynamodb package. Here is an example of how you can define the table and grant read/write permissions to the Lambda functions in the TodolistAppStack class:

import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';

export class TodolistAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Define the API Gateway construct
    const api = new apigateway.RestApi(this, 'TodoListAPI', {
      restApiName: 'TodoListAPI',
    });

    // Define the "get-tasks" resource
    const getTasks = api.root.addResource('get-tasks');
    const getTasksMethod = getTasks.addMethod('GET');

    // Define the "save-task" resource
    const saveTask = api.root.addResource('save-task');
    const saveTaskMethod = saveTask.addMethod('POST');

    // Define the "delete-task" resource
    const deleteTask = api.root.addResource('delete-task');
    const deleteTaskMethod = deleteTask.addMethod('DELETE');

    // Define the Lambda function
    const todoListLambda = new lambda.Function(this, 'TodoListLambda', {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset('lambda/todolist'),
      handler: 'index.handler',
    });

    // Add Lambda integrations for the API methods
    getTasksMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
    saveTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
    deleteTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));

	const dynamoDb = new dynamodb.DynamoDB(this, 'TodoListTable', {
	  // Additional DynamoDB table configuration options
	});

	const todoListTable = new dynamodb.Table(this, 'TodoList', {
	  // Table name and other properties
	  tableName: 'TodoList',
	  partitionKey: {
	    name: 'id',
	    type: dynamodb.AttributeType.STRING
	  }
	  // Other configuration options
	});

	todoListTable.grantReadWriteData(todoListLambda);
  }
}

Writing AWS Lambda Function Code to Handle Requests

Now, we must write Lambda function code in order to actually process API requests.

Start by creating a lambda directory and a file to store function code.

$ cd todolist-app
$ mkdir lambda
$ cd lambda
$ mkdir todolist
$ cd todolist
$ touch index.js

Now, open the file and start adding code. We will begin by adding a skeleton which uses a switch statement to call helper functions based on the value of the resource key in the event request:

exports.handler = async (event) => {
  console.log(`Received event: ${JSON.stringify(event)}`);

  // Define helper functions for each resource
  const resourceHandlers = {
    'get-tasks': getTasks,
    'save-task': saveTask,
    'delete-task': deleteTask
  };

  // Call the appropriate helper function based on the resource key in the request
  const resource = event.requestContext.resourcePath;
  const handler = resourceHandlers[resource];
  return handler ? handler(event) : handleError(new Error(`Unsupported resource: ${resource}`));
};

// Helper function for the "get-tasks" resource
const getTasks = async (event) => {
  // Get the data from the DynamoDB table
  const data = await getDataFromTable();

  // Return the data in the response
  return {
    statusCode: 200,
    body: JSON.stringify(data)
  };
};

// Helper function for the "save-task" resource
const saveTask = async (event) => {
  // Parse the JSON data in the request body
  const taskData = JSON.parse(event.body);

  // Save the data to the DynamoDB table
  await saveDataToTable(taskData);

  // Return an empty response
  return {
    statusCode: 200,
    body: ''
  };
};

// Helper function for the "delete-task" resource
const deleteTask = async (event) => {
  // Parse the JSON data in the request body
  const taskData = JSON.parse(event.body);

  // Delete the data from the DynamoDB table
  await deleteDataFromTable(taskData);

  // Return an empty response
  return {
    statusCode: 200,
    body: ''
  };
};

// Helper function to handle errors
const handleError = (err) => {
  // Log the error
  console.error(err);

  // Return an error response
  return {
    statusCode: 500,
    body: JSON.stringify({
      error: err.message
    })
  };
};

We will also need to implement the helper functions, such as getDataFromTable, saveDataToTable, and deleteDataFromTable, to perform the desired actions on the DynamoDB table.

Next, we must implement our helper functions which are being called by the switch statement. We must import a DynamoDB DocumentClient, and then implement it as follows:

// Import the AWS SDK and the DynamoDB client
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
...

// Helper function to get all items from the DynamoDB table
const getDataFromTable = async () => {
  // Define the parameters for the query
  const params = {
    TableName: 'TodoList'
  };

  // Use the scan method to retrieve all items from the table
  const result = await dynamoDb.scan(params).promise();

  // Return the items
  return result.Items;
};
...

In this code, the DynamoDB client is imported and instantiated, and then the scan method is used to retrieve all items from the TodoList table. The items are then returned from the function.

You can then use this helper function in the main Lambda handler function to return the data in the response to the get-tasks resource request.

We can do the same with save-task and delete-task. To implement the helper functions for the save-task and delete-task cases, you can use the following code:

// Import the AWS SDK and the DynamoDB client
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();

// Helper function to save an item to the DynamoDB table
const saveDataToTable = async (taskData) => {
  // Define the parameters for the put method
  const params = {
    TableName: 'TodoList',
    Item: taskData
  };

  // Use the put method to save the item to the table
  await dynamoDb.put(params).promise();
};

// Helper function to delete an item from the DynamoDB table
const deleteDataFromTable = async (taskData) => {
  // Define the parameters for the delete method
  const params = {
    TableName: 'TodoList',
    Key: {
      id: taskData.id
    }
  };

  // Use the delete method to delete the item from the table
  await dynamoDb.delete(params).promise();
};

Congratulations on building your first serverless stack! You have defined an API Gateway with three resources, and implemented a Lambda function to handle the requests for those resources. You have also added a DynamoDB table to store data persistently, and granted your Lambda function read/write permissions to the table.

Now that all the necessary resources have been created, you can test out your API to see if it works as expected. You can use a tool like Postman to send requests to your API and verify that the Lambda function handles the requests correctly and performs the desired actions on the DynamoDB table.

Testing with Postman

To test out your API using Postman, you can follow these steps:

  1. Install and open the Postman application on your computer.
  2. In the Postman application, create a new collection for your API by clicking the "New" button in the top left corner, and then selecting "Collection" from the dropdown menu. Give your collection a name and description, and then click the "Create" button.
  3. In the new collection, create a new request for each of the three resources in your API Gateway. To do this, click the "New" button in the top left corner, and then select "Request" from the dropdown menu. Give your request a name and select the collection where you want to save it, and then click the "Create" button.
  4. For each request, set the HTTP method and the URL of your API Gateway endpoint. For example, if your API Gateway endpoint is https://abc123.execute-api.us-east-1.amazonaws.com, you can set the URL for the get-tasks request as https://abc123.execute-api.us-east-1.amazonaws.com/get-tasks.
  5. For the save-task and delete-task requests, set the request body to a JSON object that contains the id and task_title keys, with the appropriate values for your test case. For example, if you want to save a task with the id 123 and the task title My task, you can set the request body as follows:
{
  "id": "123",
  "task_title": "My task"
}
  1. For each request, click the "Send" button to send the request to your API Gateway endpoint.
  2. After sending the request, check the response from your API to verify that it is as expected. For the get-tasks request, you should receive a response with an array of tasks in the body. For the save-task and delete-task requests, you should receive an empty response with a status code of 200.

Here are three sample requests that you can use to test your API using Postman:

  1. Get all tasks
  • Method: GET
  • URL: https://abc123.execute-api.us-east-1.amazonaws.com/get-tasks
  • Request body: N/A
  • Expected response:
{
  "statusCode": 200,
  "body": [
    {
      "id": "1",
      "task_title": "Task 1"
    },
    {
      "id": "2",
      "task_title": "Task 2"
    },
    // Other tasks
  ]
}
  1. Save a new task
  • Method: POST
  • URL: https://abc123.execute-api.us-east-1.amazonaws.com/save-task
  • Request body:
{
  "id": "123",
  "task_title": "My task"
}
  • Expected response:
{
  "statusCode": 200,
  "body": ""
}
  1. Delete a task
  • Method: DELETE
  • URL: https://abc123.execute-api.us-east-1.amazonaws.com/delete-task
  • Request body:
{
  "id": "123"
}

Expected response:

{
  "statusCode": 200,
  "body": ""
}

Summary

In conclusion, you have successfully built a serverless stack using AWS CDK with Lambda, API Gateway, and DynamoDB. You have defined an API Gateway with three resources, and implemented a Lambda function to handle the requests for those resources. You have also added a DynamoDB table to store data persistently, and granted your Lambda function read/write permissions to the table, creating a cohesive app.

Congratulations on completing this tutorial and building your first serverless stack! This is a great achievement, and it is a strong foundation for building more complex and powerful serverless applications in the future.

Next steps

If you're interested in learning more about the basics of coding and software development, check out our Coding Essentials Guidebook for Developers, where we cover the essential languages, concepts, and tools that you'll need to become a professional developer.

Thanks and happy coding! We hope you enjoyed this article. If you have any questions or comments, feel free to reach out to jacob@initialcommit.io.

References

  1. CDK Reference Documentation - https://docs.aws.amazon.com/cdk/api/v2/](https://docs.aws.amazon.com/cdk/api/v2/
  2. Sending requests with Postman - https://learning.postman.com/docs/getting-started/sending-the-first-request/

Final Notes