javascript

How to Build AWS Lambdas with TypeScript

Camilo Reyes

Camilo Reyes on

How to Build AWS Lambdas with TypeScript

Serverless computing is an exciting alternative to hosting apps on the AWS cloud. In this four-part series, we’ll run through how to build AWS Lambdas with TypeScript, improve the dev experience, optimize it, and finally, use AWS Cognito for security.

In this take, I would like to take you on a journey to explore AWS Lambdas using TypeScript. We will build a pizza API, use Claudia to help deploy the app, and use the AWS CLI tool to set up a DynamoDB database.

But before we dive into the project, let’s quickly touch on what serverless is and why it’s often a great option for Node.

What Is Serverless?

The concept of serverless computing isn’t exactly new — you only pay-per-use without renting or buying dedicated servers. A serverless app is not long-lived because there are no guarantees on the hardware itself. This means your code runs best when it spins up quickly, does its job, and then dies. The more lightweight the code, the less you pay.

This serverless birth and rebirth mantra is what pushes developers to come up with simpler ways to solve the problem.

Why Node.js Works Well with Serverless

Node is a nice fit for serverless because it is designed to spin up quickly and runs anywhere, including phone hardware. Over the last decade, the JavaScript V8 engine has evolved to run as efficiently as possible, which is great for serverless.

Initial Setup: Build a Pizza API and Your TypeScript Project

The entire sample code for this project is available on GitHub. You can also follow along below — we will guide you step-by-step.

To start building your pizza API, open a console and type:

> mkdir typescript-aws-lambda
> cd typescript-aws-lambda
> mkdir part-1 && cd part-1
> mkdir pizza-api && cd pizza-api

Inside your new folder, execute npm init to create the skeleton project.json file. Be sure to give it a meaningful name, like pizza-api, as it will become the name of your Lambda function once it deploys to AWS.

Note that pizza-api is now the root folder, where you will perform the remaining work.

Next, we'll install Claudia. Claudia is a severless tool that will make it easy for us to deploy our project to AWS Lambda and API Gateway. Install all project dependencies:

> npm i typescript claudia --save-dev
> npm i @aws-sdk/client-dynamodb claudia-api-builder slugify --save

These are both dev and project dependencies. A general good practice is to keep them separate in the package.json file.

Now, initiate a TypeScript config for this project. There are two options:

  1. tsc --init if you have it installed globally.
  2. Or, my preference is to do node_modules/.bin/tsc --init from the root folder.

Once you have a tsconfig.json file, enable outDir and set it to ./dist in the JSON file.

Unfortunately, Claudia does not include type definitions for TypeScript. This problem can be solved by simply defining your own types in the code repo. To do so, create a folder under the root folder @types/claudia-api-builder, then an index.d.ts file. This instructs the compiler to find type definitions for claudia-api-builder under the same codebase.

Put this in index.d.ts:

declare module "claudia-api-builder" {
  export class ApiResponse {
    constructor(response: any, headers: any, code: number);
  }
 
  export default class ApiBuilder {
    get(route: string, handler: Function, options: any = {}): void;
    post(route: string, handler: Function, options: any = {}): void;
    put(route: string, handler: Function, options: any = {}): void;
    delete(route: string, handler: Function, options: any = {}): void;
    head(route: string, handler: Function, options: any = {}): void;
    patch(route: string, handler: Function, options: any = {}): void;
    registerAuthorizer(name: string, config: any): void;
  }
}

Luckily, the code Claudia exposes is relatively trivial. You create an ApiBuilder, then declare GET/POST endpoints in the API. Claudia takes care of the rest of the work in AWS.

The ApiResponse is also included. This response class is useful for custom API responses, but there is actually a sleeker way of handling responses (so we won’t use this response class).

AWS Configuration

Before we can start playing with Claudia and AWS Lambdas, you will need three things: the AWS CLI tool, an IAM user identity, and programmatic access via a key and secret.

First, download the AWS CLI tool to create a profile folder under the user’s home folder. In Windows, for example, this folder is under C:\Users\<user>\.aws. Inside are two files: config and credentials.

Then, create an AWS account, log in, and make a note of the region. You will want to pick one closest to your physical location. For example, us-east-1 or eu-central-1. Open up the config file and set the region accordingly.

For example:

[default]
region = us-east-1
output = json

Note that the CLI tool uses the default profile in this config file. You can have many profiles, but that is beyond the scope of this guide.

Next, create a user in IAM and assign programmatic access via an access key and secret. Be sure to set the correct group/role, so that you are allowed to create Lambdas and configure the AWS gateway.

This is what your credentials file should look like:

[default]
aws_access_key_id = ACCESS_KEY
aws_secret_access_key = SECRET_ACCESS_KEY

To test the credentials, simply run:

> aws sts get-caller-identity

With this, the aws CLI tool should be able to access the AWS cloud under your account. This is a one-time setup, and you will not have to revisit this configuration. The CLI tool does a good job of staying out of the way without eating your local machine.

Build Your Initial API with TypeScript

Now, let’s move on to the fun part! Create a single endpoint in AWS to test your capabilities in spinning up new Lambdas. This will have a simple greeting response.

Create a file named api.ts in the root folder and put this in place:

import Api from "claudia-api-builder";
 
const api = new Api();
 
api.get("/", () => ["Welcome to AWS"]);

There are a couple of things going on here: a route and a callback handler with the proper JSON response. Claudia makes building APIs somewhat easy because it automates the rest of the configuration in AWS. The route translates into an entry in the AWS gateway, and the handler is the actual Lambda function.

You can think of a Lambda as a single event that fires in the AWS cloud. An event can be triggered by pretty much anything — for example, a database change or a JWT authorizer. In this case, the AWS gateway picks up an HTTP request, and it too fires an event to execute your Lambda function.

To automate AWS deployments in Claudia, you will need to run the claudia command. Open up the package.json file and put this in the scripts section:

{
  "precreate": "tsc",
  "preupdate": "tsc",
  "create": "claudia create --region us-east-1 --api-module dist/api",
  "update": "claudia update --cache-api-config apiConfig",
  "type-check": "tsc"
}

The pre prefix guarantees the latest successful build before Claudia deploys. This is a nice technique for developers who are new to TypeScript and forget to run the compiler.

Be sure to specify the correct region that is closest to you in the command line argument.

With this in place, execute npm run create and let Claudia deploy. Claudia uses a zip deploy via npm pack to upload artifacts into the AWS cloud. If everything goes well, there should be an endpoint in the output that you can copy-paste into a browser.

Something like this:

{
  "url": "https://API_GATEWAY_ID.execute-api.us-east-1.amazonaws.com/latest"
}

Connect to DynamoDB and Use Claudia

The next step is to talk to DynamoDB. This requires two things: a role with access to the database and a table on AWS.

The command to create a role fails without a policy document file in roles/dynamodb.json. Create a file under this path and define the role access:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:Scan",
        "dynamodb:DeleteItem",
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

To set up a role, run this command via the AWS CLI tool:

> aws iam put-role-policy --role-name pizza-api-executor --policy-name PizzaApiDynamoDB --policy-document file://./roles/dynamodb.json

Next, create the necessary table. We will use a url field as the key in the table. This is the resource locator field used to call the API and is part of the URL (for example, /Pepperoni-Pizza).

> aws dynamodb create-table --table-name pizzas --attribute-definitions AttributeName=url,AttributeType=S --key-schema AttributeName=url,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 --region us-east-1 --query TableDescription.TableArn --output text

Be sure to specify the correct region, the one closest to you.

Create a TypeScript file — api.ts — in the root folder. Then, fill out the import/export code:

import Api from "claudia-api-builder";
import {
  DynamoDBClient,
  GetItemCommand,
  PutItemCommand,
  ScanCommand,
} from "@aws-sdk/client-dynamodb";
import slugify from "slugify";
 
// The rest of the code goes here
 
export = api;

Lastly, build the GET/POST endpoints in api.ts. I’ll leave the endpoint that returns all pizzas using a table scan as an exercise for you. You can find the full sample code in the GitHub repo.

api.post(
  "/pizzas",
  async (request: any) => {
    const { body } = request;
 
    if (
      !body ||
      !body.name ||
      !body.ingredients ||
      !Array.isArray(body.ingredients) ||
      body.ingredients.length === 0
    ) {
      throw new Error("To make a pizza include name and ingredients");
    }
 
    const pizzaItem = {
      url: { S: slugify(body.name) },
      name: { S: body.name },
      ingredients: { SS: body.ingredients }, // type-sensitive
    };
 
    await client.send(
      new PutItemCommand({
        TableName: "pizzas",
        Item: pizzaItem,
      })
    );
 
    console.log("Pizza is saved!", pizzaItem);
    return pizzaItem;
  },
  {
    success: 201,
    error: 400,
  }
);
 
api.get(
  "/pizzas/{url}",
  async (request: any) => {
    const url = request.pathParams.url;
 
    const res = await client.send(
      new GetItemCommand({
        TableName: "pizzas",
        Key: { url: { S: url } },
      })
    );
    const pizzaItem = res.Item;
 
    if (pizzaItem === undefined) throw new Error("Pizza not found!");
 
    return pizzaItem;
  },
  {
    success: 200,
    error: 404,
  }
);

To update the Lambda in AWS, run the npm run update command. Claudia keys off a claudia.json file and updates the Lambda with the latest code.

Each method takes three parameters: route, handler, and options. The options parameter at the end specifies the status code in the HTTP response. For example, for a GET, it throws an exception when the pizza is missing. Claudia handles this exception automatically and turns it into a '404 Not Found' response. Similarly, with POST, you get a '400 Bad Request' response when an exception gets thrown or '201 Created' when a new resource is available.

The request parameters contain either the body of the request or the pathParams values. These parameters come from the event fired by the AWS gateway, and Claudia makes it easy for you to consume both of them in your Lambda function.

Test Out Your AWS Lambda with CURL

Drumroll, please!

Now we’ll use CURL to test our Lambda. Send a POST request first to make a pizza with a name and ingredients:

> curl -X POST -i -H "Accept-Type: application/json" -H "Content-Type: application/json" -d "{\"name\":\"Pepperoni Pizza\",\"ingredients\":[\"tomato sauce\",\"cheese\",\"pepperoni\"]}" https://API_GATEWAY_ID.execute-api.us-east-1.amazonaws.com/latest/pizzas

Feel free to play around with this. If you forget to add a name or include ingredients in the body, the test will fail. DynamoDB is type-sensitive, so even if you try to put a list of numbers in the ingredients, it fails the request.

Finally, fire a GET to get your tasty pizza:

> curl -X GET -i -H "Accept-Type: application/json" -H "Content-Type: application/json" https://API_GATEWAY_API.execute-api.us-east-1.amazonaws.com/latest/pizzas/Pepperoni-Pizza

Up Next: Improve the Dev Experience

In this post, we explored how to effortlessly build AWS Lambda functions using TypeScript with Claudia.

Next in the series, we’ll look at ways to improve the developer experience, so your code is easier to work with and more robust.

Until then, happy coding!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Share this article

RSS
Camilo Reyes

Camilo Reyes

Our guest author Camilo is a Software Engineer from Houston, Texas. He’s passionate about JavaScript and clean code that runs without drama. When not coding, he loves to cook and work on random home projects.

-> All articles by Camilo Reyes-> Become an AppSignal author

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps