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:
tsc --init
if you have it installed globally.- 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.