Header Image Using TypeScript with AWS Lambda and SAM

Using TypeScript with AWS Lambda and AWS SAM

I was pretty late to the TypeScript party but now that I've started using it it's hard to write JavaScript without it.

Lambda functions are often relatively simple, so requiring types and all of the goodness that comes with TypeScript, might be overkill. However, it's actually relatively easy to add TypeScript to an AWS SAM Lambda function. So in this article, we'll cover the simplest option possible to get started using TypeScript with AWS SAM.

A Sample Project

In a previous blog post (What is AWS SAM?) we created a simple Lambda function that could be accessed via AWS API Gateway. The API Gateway would proxy the request to AWS Lambda, execute the Lambda function and return the response.

In this case the response was "Hello World" or "Hello Steve" if you passed ?name=Steve as a query string parameter.

The code from the last blog is available on Github, the typescript branch contains the changes mentioned in this post.

Adding TypeScript to our Lambda Function

To get started we're going to install TypeScript and the Default Types for NodeJS. We're also going to install the @types/aws-lambda package. The package is a set of AWS Lambda Types that are just a bonus to help work with Lambda.

npm install --save-dev typescript @types/node @types/aws-lambda

With those additional packages install as development dependancies we'll add our TypeScript config tsconfig.json file to the root of our project.

{
  "compilerOptions": {
    "lib": ["es2020"],
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "es2020",

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    "outDir": "dist",
    "rootDir": "./src",
  },
  "include": [
    "./src/**/*.ts",
    "./src/**/*.js"
  ]
}

Since we're targeting Node14, the current LTS node version, we can target ES2020. It's also important to note that our input directory is ./src and our output directory is dist. Using dist just helps to avoid confusion with the sam build directory. Now our TypeScript code will be compiled from the src directory and placed into the dist directory.

As an asside we could have used @tsconfig/node14. This awesome little package provides sensible defaults for targeting each version of NodeJS. However, we set the config explicitly for clarity.

Finally, it's important to note that we're using commonjs for the module code generation. This is critical because AWS Lambda will attempt to require the JavaScript lambda function in order to execute it. When our code is compile this will add the important exports.helloFromLambdaHandler = helloFromLambdaHandler; line to our code.

Changing the code to add Types

Then we'll make the following changes:

exports.helloFromLambdaHandler = async (event, context) => {
  const name = event.queryStringParameters?.name || "world";

  return {
    "statusCode": 201,
    "body": JSON.stringify({
      message: `Hello ${name}`
    })
  };
};
import  { Handler, APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"

type ProxyHandler = Handler<APIGatewayProxyEventV2, APIGatewayProxyResultV2>

export const helloFromLambdaHandler : ProxyHandler = async (event, context) => {
  const name = event.queryStringParameters?.name || "world";

  return {
    "statusCode": 201,
    "body": JSON.stringify({
      message: `Hello ${name}`
    })
  };
};

We've also renamed the file from hello-from-lambda.js to hello-from-lambda.ts.

First we've imported the types from the @types/aws-lambda (AWS Lambda Types) package. Then we've created a custom type called ProxyHandler from the Generic Handler type which expects a APIGatewayProxyEventV2 for the Event Type (TEvent) and APIGatewayProxyResultV2 for the return Type (TResult).

Since our Lambda function is for an API Gateway Proxy function this will help give us the correct types.

Finally we've used export const and assigned the ProxyHandler Type to our helloFromLambdaHandler function. Since we declared "module":"commonjs" in our tsconfig this will ensure the correct exports are created.

The full changeset is as follows:

--- a/src/handlers/hello-from-lambda.js
+++ b/src/handlers/hello-from-lambda.ts
@@ -1,4 +1,8 @@
-exports.helloFromLambdaHandler = async (event, context) => {
+import  { Handler, APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"
+
+type ProxyHandler = Handler<APIGatewayProxyEventV2, APIGatewayProxyResultV2>
+
+export const helloFromLambdaHandler : ProxyHandler = async (event, context) => {

This is a pretty simple example so the conversion may not have been worth the effort. However, as the project grows, you can enjoy the benefits of TypeScript.

Compiling to JavaScript

Now that we've got everything in place we can compile our code with tsc. We could go ahead and just type that in the command prompt but I'd like to add a couple of commands to NPM.

Lets open our package.json file and add the following:

{
  //...
  "devDependencies": {
    "@types/aws-lambda": "^8.10.81",
    "@types/node": "^16.4.11",
    "jest": "^26.6.3",
    "typescript": "^4.3.5"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w"
  },
  "files": [
    "dist/"
  ]
}

Here we've added a npm run build and an npm run watch command. We've also signaled that we'll output the dist/ folder instead of the src folder.

Personally I like to use the watch command so that my code keeps being transpiled as I edit the TypeScript code.

--- a/template.yml
+++ b/template.yml
@@ -22,7 +22,8 @@ Resources:
   helloFromLambdaFunction:
     Type: AWS::Serverless::Function
     Properties:
-      Handler: src/handlers/hello-from-lambda.helloFromLambdaHandler
+      Handler: dist/handlers/hello-from-lambda.helloFromLambdaHandler

Now all that's left is to ensure that the CloudFormation template.yml file is taking code from the dist folder.

When we now run sam build and start-api we should be seeing our TypeScript code:

sam build --use-container && sam local start-api

Let's head to http://localhost:3000/hello and check it's all working 🤞 (remember to compile if you're not running tsc -w already). What's really cool is that if we run npm run watch the code will automatically transpile into the dist folder. When we started sam local we will have seen the following message:

You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template

With this in place, unless we change the template.yml file whenever we change the TypeScript code it will be transpile and automatically updated in our HTTP API.

2021-08-27
Steve Smith