Build a basic serverless application using AWS CDK
Published: 2020-08-25 06:59:45

---
title: Build a basic serverless application using AWS CDK
published: true
date: 2020-08-24 20:18:15 UTC
tags: 
canonical_url: https://taimos.de/blog/build-a-basic-serverless-application-using-aws-cdk
---

In this blog post, I want to show you how to write a basic Serverless application using the AWS CDK to deploy an API Gateway, Lambda functions, and a DynamoDB table. We will be using the asset support of the CDK to bundle our business logic during the deployment.

## What are we implementing?

Our application will be a straightforward ToDo list that supports adding new items to the list and displaying it. Further actions are left to the reader to experiment. We will store the data in a DynamoDB table and implement the business logic using AWS Lambda with our code written in TypeScript. As the API endpoint, an HTTP API will be used.

## Initialize your CDK application

To start our project, we need to initialize our CDK project. In my previous blog post, I describe the necessary steps.

After these steps are done, we enhance the setup by installing further libraries with the following command:

```
npm install --save-exact @aws-cdk/aws-lambda @aws-cdk/aws-lambda-nodejs @aws-cdk/aws-dynamodb @aws-cdk/aws-apigatewayv2
```

## Creating the datastore

To create our backing datastore we create a new DynamoDB table and use `PK` as the hash key of the table. In CDK, we accomplish this by using the Table construct. To ease the setup, we use on-demand billing for DynamoDB, so we do not need to calculate throughput. Be aware that this is not covered by the Free Tier of your AWS account!

```
const table = new dynamodb.Table(this, 'Table', {
  partitionKey: {
    name: 'PK',
    type: dynamodb.AttributeType.STRING,
  },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
```

To be able to look into the database, later on, we create an output to know the table name CloudFormation chose.

```
new cdk.CfnOutput(this, 'TableName', {value: table.tableName});
```

## Creating our business logic

For our business logic, we create two lambda functions—one to fetch a list of all tasks and one to create a new task inside the table. To hold the code of these lambdas, we create a folder `lambda/` and initialize it as a new NodeJS project. I am using the following settings to create my Lambda in TypeScript:

```
{
  "name": "lambda",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "tslint -p tsconfig.json && jest"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.714.0",
    "uuid": "^8.3.0"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.59",
    "@types/jest": "^26.0.4",
    "@types/node": "14.0.23",
    "@types/uuid": "^8.3.0",
    "jest": "^26.1.0",
    "sinon": "^9.0.2",
    "ts-jest": "^26.1.2",
    "ts-mock-imports": "^1.3.0",
    "ts-node": "^8.10.2",
    "tslint": "^6.1.3",
    "typescript": "~3.9.6"
  }
}

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "declaration": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "esModuleInterop": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "typeRoots": ["./node_modules/@types"]
  },
  "exclude": ["cdk.out"]
}
```

The most important part is to have the AWS SDK and the uuid library installed.

The header of my lambda code, sitting in `lib/tasks.ts`, is:

```
import { DynamoDB } from 'aws-sdk';
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { env } from 'process';
import { v4 } from 'uuid';

const dynamoClient = new DynamoDB.DocumentClient();
```

To add new tasks to the database, we will post a JSON object to the API with two fields named "name" and "state". We generate a new UUID to be used as the primary id of the task.

```
// Export new function to be called by Lambda
export async function post(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
    // Log the event to debug the application during development
  console.log(event);

    // If we do not receive a body, we cannot continue...
  if (!event.body) {
        // ...so we return a Bad Request response
    return {
      statusCode: 400,
    };
  }

    // As we made sure we have a body, let's parse it
  const task = JSON.parse(event.body);
    // Let's create a new UUID for the task
  const id = v4();

    // define a new task entry and await its creation
  const put = await dynamoClient.put({
    TableName: env.TABLE_NAME!,
    Item: {
            // Hash key is set to the new UUID
      PK: id,
            // we just use the fields from the body
      Name: task.name,
      State: task.state,
    },
  }).promise();

    // Tell the caller that everything went great
  return {
    statusCode: 200,
    body: JSON.stringify({...task, id}),
  };
}
```

To fetch data from the table, we implement another function that resides in the same file. For better readability, I split it into two functions, one to handle the API call and a helper function that is reading from the database.

```
// Export new function to be called by Lambda
export async function get(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
  // Log the event to debug the application during development
  console.log(event);

  // Get a list of all tasks from the DB, extract the method to do paging
  const tasks = (await getTasksFromDatabase()).map((task) => ({
    // let's reformat the data to our API model
    id: task.PK,
    name: task.Name,
    state: task.State,
  }));

  // Return the list as JSON objects
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    // Body needs to be string so render the JSON to string
    body: JSON.stringify(tasks),
  };
}

// Helper method to fetch all tasks
async function getTasksFromDatabase(): Promise<DynamoDB.DocumentClient.ItemList> {
  // This variable will hold our paging key
  let startKey;
  // start with an empty list of tasks
  const result: DynamoDB.DocumentClient.ItemList = [];

  // start a fetch loop
  do {
    // Scan the table for all tasks
    const res: DynamoDB.DocumentClient.ScanOutput = await dynamoClient.scan({
      TableName: env.TABLE_NAME!,
      // Start with the given paging key
      ExclusiveStartKey: startKey,
    }).promise();
    // If we got tasks, store them into our list
    if (res.Items) {
      result.push(...res.Items);
    }
    // Keep the new paging token if there is one and repeat when necessary
    startKey = res.LastEvaluatedKey;
  } while (startKey);
  // return the accumulated list of tasks
  return result;
}
```

After we put this code into the `tasks.ts` file, we can instantiate the functions inside our CDK app and configure automatic bundling.

To setup the Lambda functions, the NodejsFunction construct is excellent. We can specify the file to use as the entry point and the name of the exported function. CDK will then transpile and package this code during synth using Parcel. For this step, you need a running Docker setup on your machine as the bundling happens inside a container.

```
const postFunction = new lambdaNode.NodejsFunction(this, 'PostFunction', {
  runtime: lambda.Runtime.NODEJS_12_X,
  // name of the exported function
  handler: 'post',
  // file to use as entry point for our Lambda function
  entry: __dirname + '/../lambda/lib/tasks.ts',
  environment: {
    TABLE_NAME: table.tableName,
  },
});
// Grant full access to the data
table.grantReadWriteData(postFunction);

const getFunction = new lambdaNode.NodejsFunction(this, 'GetFunction', {
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'get',
  entry: __dirname + '/../lambda/lib/tasks.ts',
  environment: {
    TABLE_NAME: table.tableName,
  },
});
// Grant only read access for this function
table.grantReadData(getFunction);
```

## Setup of the API

As the entry point to our API, we use the new HTTP API of the service API Gateway. To create the API and print out the URL, we use this code:

```
const api = new apiGW.HttpApi(this, 'Api');
new cdk.CfnOutput(this, 'ApiUrl', {value: api.url!});
```

We then need to add routes for every path and method we want to expose and supply a Lambda function that implements this.

```
api.addRoutes({
  path: '/tasks',
  methods: [apiGW.HttpMethod.POST],
  integration: new apiGW.LambdaProxyIntegration({handler: postFunction})
});
api.addRoutes({
  path: '/tasks',
  methods: [apiGW.HttpMethod.GET],
  integration: new apiGW.LambdaProxyIntegration({handler: getFunction})
});
```

## Deploy to our AWS account

By running `cdk synth`, we can synthesize the CloudFormation template and let CDK package our Lambda code. The `cdk.out` folder will then contain the code bundles and the templates, ready to be deployed.

![Bundling](https://dev-to-uploads.s3.amazonaws.com/i/e1x3aut9m6dm6vp0rlm2.png)

Using the `cdk deploy` command, our application will go live inside our AWS account and print out the table name of our DynamoDB table and the API URL.

![Deploy](https://dev-to-uploads.s3.amazonaws.com/i/vtlvc4vkz71lng844m1e.png)

## Testing the API

We can then use Postman or cURL to add tasks to the database or retrieve the list of entries.

```
API_URL=https://XXXXX.execute-api.eu-central-1.amazonaws.com/

# Add new task to the table
curl -v -X POST -d '{"name":"Testtask","state":"OPEN"}' -H "Content-Type: application/json" ${API_URL}tasks

# Retrieve the list
curl -v ${API_URL}tasks
```

## Conclusion

In this post, we learned to set up an AWS CDK application that creates a DynamoDB table, two Lambda functions, and an HTTP API. It uses CDK native bundling of code to transpile TypeScript to JavaScript and to package the code ready to be deployed to Lambda.