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.