Using Typescript and Esbuild to deploy a Lambda Function with AWS CDK

Using Typescript and Esbuild to deploy a Lambda Function with AWS CDK
Photo by SpaceX on Unsplash

Photo by SpaceX on Unsplash

In this previous post, we saw how to deploy a Lambda function using the CDK. The source of the Lambda was pointing to an index.js file with the following code:

exports.handler = async function(event) {
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/json" },
    body: JSON.stringify({ message: "Hello from my Lambda node!" })
  };
};

As we can see, the code is a simple "hello world" and far from a real-world application. Indeed in an actual application, it is more complex since you add external dependencies, use Typescript, add images, etc. hence, we might wonder:

  • How to deploy a lambda function that relies on NPM modules?
  • How to deploy when we use NPM modules and Typescript?

These are the two questions we are going to answer in this tutorial.

A CDK construct called AWS Lambda Nodejs helps us achieve our goal. When running the command cdk deploy, it will first build our project with all the required dependencies then deploy the output in the cloud.

Docker and ESbuild are used under the hood to build the application:

  • Docker: As you might guess, it requires having Docker installed on the computer where the build will be done.
  • Esbuild: It is an extremely fast JavaScript bundler and minifier that supports Typescript files.

If the CDK detects Esbuild on the computer during the deployment, it will build the source files using it.

Prerequisites

To continue this tutorial, make sure you have the following tools installed on your computer:

  • An AWS account to deploy our Lambda function
  • AWS CLI configured (check out this link to see how to do it)
  • Node.js 12+ with NPM or Yarn
  • Docker to test the Lambda locally using AWS SAM CLI
  • AWS CDK installed locally: npm install -g cdk

What we will build

We will build a Lambda function that receives a country then returns the current time for this latter. It will be enough to show how to apply it to a real-world application.

Create the CDK project

Create a folder, then initialize an AWS CDK Typescript project:

mkdir node-lambda-timeviewer
cd node-lambda-timeviewer
cdk init sample-app --language typescript

Once installed, let's install the CDK module to build Lambda function for Node.js and Esbuild:

yarn add @aws-cdk/aws-lambda-nodejs 
yarn add -D esbuild
# or
npm install @aws-cdk/aws-lambda-nodejs 
npm install --save-dev esbuild

Update the content of the lib/node-lambda-timeviewer-stack.ts with the code below:

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda-nodejs';

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

    new lambda.NodejsFunction(this, 'TimeViewer', {
      entry: '../src/time-viewer.ts', // accepts .js, .jsx, .ts and .tsx files
      functionName: 'timeViewer',
      handler: 'handler',
      memorySize: 512,
      timeout: cdk.Duration.seconds(10)
    });
  }
}

The code above creates a Lambda function with some configuration options. The most important here is the "entry" properties; the value is the file's path containing the Lambda function handler.
We will write the logic of the Lambda function inside this file.

Remove the unused dependencies that come with the default project:

yarn remove @aws-cdk/aws-sns @aws-cdk/aws-sns-subscriptions @aws-cdk/aws-sqs

Write the logic of the Lambda function

We will install two packages:

We will also install types definition for AWS Lambda.

yarn add date-fns date-fns-tz @vvo/tzdb
yarn add -D @types/aws-lambda

At root project directory, create a folder named src then, add the file time-viewer.ts

mkdir src
cd src
touch time-viewer.ts

Open the file and add the code below:

import { APIGatewayProxyHandler } from 'aws-lambda';
import { getTimeZones } from "@vvo/tzdb";
import { utcToZonedTime, format } from 'date-fns-tz';

export const handler: APIGatewayProxyHandler = async (event) => {
  const country = 'France'

  const timeZone = getTimeZones().find((timeZone) => timeZone.countryName === country);

  if (!timeZone) {
    return {
      statusCode: 400,
      headers: { "Content-Type": "text/json" },
      body: JSON.stringify({ message: "Timezone not found for this country!" })
    };
  }

  const zonedDate = utcToZonedTime(new Date(), timeZone.name)
  const pattern = 'dd-MM-yyyy HH:mm:ss.SSS \'GMT\' XXX (z)'
  const output = format(zonedDate, pattern, { timeZone: timeZone.name })

  return {
    statusCode: 200,
    headers: { "Content-Type": "text/json" },
    body: JSON.stringify({ time: output })
  };
};

To test the code locally, we need a tool that makes the interaction between AWS CDK and AWS SAM CLI possible. Check out this post to see how to install it.

Below is the code to execute our Lambda function locally:  

sam-beta-cdk local invoke NodeLambdaTimeviewerStack/TimeViewer

We got the following output:

Execute Lambda function locally

There are two things to highlight here:

1- Green rectangle: Docker pulls the image for local SAM CLI emulation from the AWS public ECR repository.

2- Red rectangle: After the execution, we see the output returned by the Lambda function, which is the current time in France when I write this post.

Configure Esbuild options

When creating your Lambda, you can define options to use Esbuild to compile your source code. Let's update our Lambda to add some options:

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

    new lambda.NodejsFunction(this, 'TimeViewer', {
      entry: path.resolve(__dirname, '../src/time-viewer.ts'), // accepts .js, .jsx, .ts and .tsx files
      functionName: 'timeViewer',
      handler: 'handler',
      memorySize: 512,
      timeout: cdk.Duration.seconds(10),
      bundling: {
        minify: true, // minify code, defaults to false
        sourceMap: true, // include source map, defaults to false
        sourceMapMode: lambda.SourceMapMode.INLINE, // defaults to SourceMapMode.DEFAULT
        sourcesContent: false, // do not include original source into source map, defaults to true
        target: 'es2020', // target environment for the generated JavaScript code
        define: { // Replace strings during build time
          'process.env.COUNTRY': JSON.stringify('France'),
        },
      }
    });
  }
}

Update the file time-viewer.ts to read the country from the environment variable

change: const country = 'France';
to: const country = process.env.COUNTRY;

Execute the Lambda function again to see the result:

Execute Lambda function locally with Esbuild build options added

Deploy in production

The command to deploy in production is still the same so, just do :

cdk synth
cdk deploy

Wait for the deployment to complete and connect to AWS console, go to the Lambda page, and execute your Lambda function:

Execute the Lambda function in the AWS console

Wrap up

With the possibility to build a sources project before deploying them in production, The Lambda CDK construct for Node.js enforce the integration of CDK in the developer experience. There is no need to have your project apart of the CDK project and write a bash script for the deployment. The command cdk deploy doesn't everything for you.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to not miss the upcoming posts and the tips and tricks I share every week.

Happy to see you soon ?