javascript

Build Serverless APIs with Node.js and AWS Lambda

Ayooluwa Isaiah

Ayooluwa Isaiah on

Last updated:

Build Serverless APIs with Node.js and AWS Lambda

The post was updated on 9 August 2023 to use the current LTS version of Node (v18), AWS CLI (2.13.3) and SAM CLI (1.94.0).

AWS Lambda has been around for a few years now, and it remains the most popular way to experiment with serverless technology. If you're not familiar with serverless, it's a model of development in which managing, provisioning, and scaling servers is abstracted away from application development. Servers do exist in a serverless world, but they are completely managed by the cloud provider, allowing developers to focus on packaging their code for deployment.

AWS Lambda is a type of Function-as-a-Service (FaaS) offering that allows code execution on demand in response to preconfigured events or requests. This post will introduce you to AWS Lambda and guide you on creating and deploying Lambda functions with Node.js and AWS SAM.

Let's get started!

Prerequisites

Before you proceed with this tutorial, ensure that you have Node.js 18.x LTS installed on your computer, as this is the latest release that AWS Lambda supports at the time of writing. However, the content of this article should stay relevant even when newer releases are supported. You can use Volta to install and manage multiple versions of Node.js on your computer. Also, ensure you sign up for a free AWS account if you don't have one already.

If you intend to run your AWS Lambda function locally (which I'll be demonstrating in this article), you'll need Docker installed on your computer. Follow these instructions to set up Docker for your operating system before proceeding with the rest of this tutorial.

Install the AWS CLI and AWS SAM CLI

In this guide, we'll be using both the AWS CLI and the AWS Serverless Application Model (SAM) CLI to develop our serverless functions and deploy them to AWS.

The former interacts with AWS services on the command line, while the latter helps with building, debugging, deploying, and invoking Lambda functions. Read more about AWS SAM in the docs.

The exact way to install both CLI tools will differ depending on your operating system. You can install or upgrade to the latest version of AWS CLI for Linux, macOS, and Windows by following the instructions on this page. To install the AWS SAM CLI, check this page for the relevant guide for your operating system.

Here are the versions of AWS CLI and AWS SAM CLI that I installed while writing this guide:

bash
$ aws --version aws-cli/2.13.3 Python/3.11.4 Linux/5.19.0-50-generic exe/x86_64.ubuntu.22 prompt/off $ sam --version SAM CLI, version 1.94.0

After you've installed both CLI tools, follow this guide to set up your AWS credentials so that you can interact successfully with your AWS account via the CLIs.

bash
$ aws configure list Name Value Type Location ---- ----- ---- -------- profile <not set> None None access_key ****************ZFEF shared-credentials-file secret_key ****************BnOU shared-credentials-file region us-east-1 config-file ~/.aws/config

Create Your First AWS Lambda Function with Node.js

Let's start by writing a simple hello world function to demonstrate how AWS Lambda works. Run the command below to initialize a new project:

bash
$ sam init --runtime nodejs18.x --name aws-lambda-nodejs-example

When prompted, choose AWS Quick Start Templates under template source, Hello World Example under application templates and also under starter templates, select N for X-Ray tracing and N for monitoring with CloudWatch.

bash
Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 Choose an AWS Quick Start application template 1 - Hello World Example 2 - Hello World Example with Powertools for AWS Lambda 3 - Multi-step workflow 4 - Standalone function 5 - Scheduled task 6 - Data processing 7 - Serverless API 8 - Full Stack 9 - Lambda Response Streaming Template: 1 Based on your selections, the only Package type available is Zip. We will proceed to selecting the Package type as Zip. Based on your selections, the only dependency manager available is npm. We will proceed copying the template using npm. Select your starter template 1 - Hello World Example 2 - Hello World Example TypeScript Template: 1 Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: N Would you like to enable monitoring using CloudWatch Application Insights? For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: N Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment) ----------------------- Generating application: ----------------------- Name: aws-lambda-nodejs-example Runtime: nodejs18.x Architectures: x86_64 Dependency Manager: npm Application Template: hello-world Output Directory: . Configuration file: aws-lambda-nodejs-example/samconfig.toml Next steps can be found in the README file at aws-lambda-nodejs-example/README.md Commands you can use next ========================= [*] Create pipeline: cd aws-lambda-nodejs-example && sam pipeline init --bootstrap [*] Validate SAM template: cd aws-lambda-nodejs-example && sam validate [*] Test Function in the Cloud: cd aws-lambda-nodejs-example && sam sync --stack-name {stack-name} --watch

Once the command exits, change into the freshly minted aws-lambda-nodejs-example folder. It should have the following folder structure:

bash
. ├── events    └── event.json ├── .gitignore ├── hello-world    ├── app.mjs    ├── .npmignore    ├── package.json    └── tests    └── unit    └── test-handler.mjs ├── README.md ├── samconfig.toml └── template.yaml

Here's a short description of the important files and directories in the project:

  • template.yaml: Defines the AWS resources for your application.
  • hello-world/app.mjs: Contains the Lambda function logic.
  • hello-world/package.json: Contains any Node.js dependencies required by the application.
  • hello-world/tests/: Contains unit tests for your Lambda functions.

Open up the template.yaml file in your text editor and take note of the following lines:

yaml
Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello-world/ Handler: app.lambdaHandler Runtime: nodejs18.x Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get

These lines describe the name of your Lambda function (HelloWorldFunction), the runtime used to execute it (nodejs18.x), and the type of trigger for the function (Api). This indicates that your Lambda function will execute when a GET request is sent to the /hello route via API Gateway. Note that there are several other ways to invoke Lambda functions.

The CodeUri line indicates that the code for the HelloWorldFunction is in the hello-world directory. The Handler property specifies app.mjs as the file with the function code, which should have a named export called lambdaHandler.

Open up the hello-world/app.mjs file and examine its content:

javascript
/** * * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format * @param {Object} event - API Gateway Lambda Proxy Input Format * * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html * @param {Object} context * * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html * @returns {Object} object - API Gateway Lambda Proxy Output Format * */ export const lambdaHandler = async (event, context) => { try { return { statusCode: 200, body: JSON.stringify({ message: "hello world", }), }; } catch (err) { console.log(err); return err; } };

This simple function takes two parameters and returns a response object containing a 'hello world' message. The first parameter is the JSON payload sent by the invoker of the function, while the second is the context object which contains information about the function invocation and execution environment. This handler is async, so you can use return or throw to return a response or an error, respectively. Non-async handlers must use a third callback parameter (not shown here).

Go ahead and modify the function to return the reserved environmental variables defined in the runtime environment instead of the hello world message.

javascript
export const lambdaHandler = async (event, context) => { try { const environmentalVariables = { handler: process.env._HANDLER, aws_region: process.env.AWS_REGION, aws_execution_env: process.env.AWS_EXECUTION_ENV, aws_lambda_function_name: process.env.AWS_LAMBDA_FUNCTION_NAME, aws_lambda_function_name: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, aws_lambda_function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION, aws_lambda_log_group_name: process.env.AWS_LAMBDA_LOG_GROUP_NAME, aws_lambda_log_stream_name: process.env.AWS_LAMBDA_LOG_STREAM_NAME, aws_lambda_runtime_api: process.env.AWS_LAMBDA_RUNTIME_API, lang: process.env.LANG, tz: process.env.TZ, lambda_task_root: process.env.LAMBDA_TASK_ROOT, lambda_runtime_dir: process.env.LAMBDA_RUNTIME_DIR, path: process.env.PATH, ld_library_path: process.env.LD_LIBRARY_PATH, }; return { statusCode: 200, body: JSON.stringify(environmentalVariables), }; } catch (err) { console.log(err); return err; } };

We can access the environmental variables through process.env and, after aggregating them in an object, return them in the response object. API Gateway uses the statusCode property to add the right HTTP status code to the generated response.

Testing Your AWS Lambda Function Locally

Before deploying your function, you'll want to test it locally to confirm it works as expected. To do so, run the following SAM command at the root of your project directory:

bash
$ sam local start-api Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET] 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. If you used sam build before running local commands, you will need to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template 2023-08-04 14:12:00 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:3000 2023-08-04 14:12:00 Press CTRL+C to quit

This command requires Docker, so make sure it's installed and running on your computer. Otherwise, you might get an error message similar to the one shown below:

plaintext
Error: Running AWS SAM projects locally requires Docker. Have you got it installed and running?

If you are still getting the above error despite Docker running, check your user permissions — you can, for instance, configure the docker command to run without sudo.

Once the application is running, make a GET request to http://localhost:3000/hello. This will cause AWS SAM to start a Docker container to run the function. Once the container is up and running, the function will execute and the following result will be returned:

bash
$ curl http://localhost:3000/hello {"handler":"app.lambdaHandler","aws_region":"us-east-1","aws_execution_env":"AWS_Lambda_nodejs18.x","aws_lambda_function_name":"128","aws_lambda_function_version":"$LATEST","aws_lambda_log_group_name":"aws/lambda/HelloWorldFunction","aws_lambda_log_stream_name":"$LATEST","aws_lambda_runtime_api":"127.0.0.1:9001","lang":"en_US.UTF-8","tz":":/etc/localtime","lambda_task_root":"/var/task","lambda_runtime_dir":"/var/runtime","path":"/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin","ld_library_path":"/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib"}

You can use jq to prettify the output if you have it installed:

bash
$ curl http://localhost:3000/hello | jq { "handler": "app.lambdaHandler", "aws_region": "us-east-1", "aws_execution_env": "AWS_Lambda_nodejs18.x", "aws_lambda_function_name": "128", "aws_lambda_function_version": "$LATEST", "aws_lambda_log_group_name": "aws/lambda/HelloWorldFunction", "aws_lambda_log_stream_name": "$LATEST", "aws_lambda_runtime_api": "127.0.0.1:9001", "lang": "en_US.UTF-8", "tz": ":/etc/localtime", "lambda_task_root": "/var/task", "lambda_runtime_dir": "/var/runtime", "path": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", "ld_library_path": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib" }

You can also test your Lambda function without making an HTTP request to trigger it. The SAM CLI provides a way to invoke the function using a predefined JSON file. Run the following command in your project root to try it out:

bash
$ sam local invoke "HelloWorldFunction" --event events/event.json Invoking app.lambdaHandler (nodejs18.x) Local image is up-to-date Using local image: public.ecr.aws/lambda/nodejs:18-rapid-x86_64. Mounting /path/to/project/aws-lambda-nodejs-example/hello-world as /var/task:ro,delegated, inside runtime container START RequestId: 03be87b5-0a7a-4dbd-8c1a-914a23b7c533 Version: $LATEST END RequestId: 03be87b5-0a7a-4dbd-8c1a-914a23b7c533 REPORT RequestId: 03be87b5-0a7a-4dbd-8c1a-914a23b7c533 Init Duration: 0.04 ms Duration: 136.93 ms Billed Duration: 137 ms Memory Size: 128 MB Max Memory Used: 128 MB {"statusCode":200,"body":"{\"handler\":\"app.lambdaHandler\",\"aws_region\":\"us-east-1\",\"aws_execution_env\":\"AWS_Lambda_nodejs18.x\",\"aws_lambda_function_name\":\"128\",\"aws_lambda_function_version\":\"$LATEST\",\"aws_lambda_log_group_name\":\"aws/lambda/HelloWorldFunction\",\"aws_lambda_log_stream_name\":\"$LATEST\",\"aws_lambda_runtime_api\":\"127.0.0.1:9001\",\"lang\":\"en_US.UTF-8\",\"tz\":\":/etc/localtime\",\"lambda_task_root\":\"/var/task\",\"lambda_runtime_dir\":\"/var/runtime\",\"path\":\"/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin\",\"ld_library_path\":\"/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib\"}"}

The event that triggers a Lambda function usually comes with a JSON payload. You should provide this payload using the --event option (as demonstrated above) to invoke the function locally. This payload passes as the first argument to the Lambda function. The event.json file is created by the SAM CLI when initializing the project, so it may be used for this purpose. Learn more about events.

When you invoke a function locally, you'll get some information on:

  • the Docker container image used to execute the function
  • how long it ran for
  • how much memory was used

You'll also get the actual return value of your function below the runtime information.

Deploying the Lambda Function to the AWS Cloud

Once you are happy with how your function runs locally, you can deploy it to the AWS Cloud through the SAM CLI. First, run sam build to generate artifacts that target AWS Lambda's execution environment:

bash
$ sam build Starting Build use cache Manifest file is changed (new hash: 09dfcb043156cef267bd83012e768966) or dependency folder (.aws-sam/deps/ee9a0bed-0e67-4e57-b95e-60fac60491b2) is missing for (HelloWorldFunction), downloading dependencies and copying/building source Building codeuri: /path/to/project/aws-lambda-nodejs-example/hello-world runtime: nodejs18.x metadata: {} architecture: x86_64 functions: HelloWorldFunction Running NodejsNpmBuilder:NpmPack Running NodejsNpmBuilder:CopyNpmrcAndLockfile Running NodejsNpmBuilder:CopySource Running NodejsNpmBuilder:NpmInstall Running NodejsNpmBuilder:CleanUp Running NodejsNpmBuilder:CopyDependencies Running NodejsNpmBuilder:CleanUpNpmrc Running NodejsNpmBuilder:LockfileCleanUp Running NodejsNpmBuilder:LockfileCleanUp Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Validate SAM template: sam validate [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch [*] Deploy: sam deploy --guided

Next, run sam deploy --guided to deploy the function, and answer the prompts as shown below:

bash
$ sam deploy --guided Configuring SAM deploy ====================== Looking for config file [samconfig.toml] : Found Reading default arguments : Success Setting default arguments for 'sam deploy' ========================================= Stack Name [aws-lambda-nodejs-example]: AWS Region [us-east-1]: #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [Y/n]: y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: n HelloWorldFunction has no authentication. Is this okay? [y/N]: y Save arguments to configuration file [Y/n]: y SAM configuration file [samconfig.toml]: SAM configuration environment [default]:

Once deployment is successful, you will see the API Gateway URL in the output. It should have the following structure:

bash
https://<API_ID>.execute-api.<AWS_REGION>.amazonaws.com/Prod/hello/

Once you make a GET request to that URL, your function will execute, and you'll get a similar output as before:

bash
$ curl https://s032akg5bh.execute-api.us-east-1.amazonaws.com/Prod/hello/ | jq { "handler": "app.lambdaHandler", "aws_region": "us-east-1", "aws_execution_env": "AWS_Lambda_nodejs18.x", "aws_lambda_function_name": "128", "aws_lambda_function_version": "$LATEST", "aws_lambda_log_group_name": "/aws/lambda/aws-lambda-nodejs-example-HelloWorldFunction-YqCrwRMczenz", "aws_lambda_log_stream_name": "2023/08/04/[$LATEST]eca21b4de7d745539f6040222356d42c", "aws_lambda_runtime_api": "127.0.0.1:9001", "lang": "en_US.UTF-8", "tz": ":UTC", "lambda_task_root": "/var/task", "lambda_runtime_dir": "/var/runtime", "path": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", "ld_library_path": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib" }

Congratulations, you've successfully deployed your first Lambda function to production!

Using NPM Packages in AWS Lambda Functions

Let's go ahead and create a new Lambda function that uses some NPM packages to perform a web scraping task. Create a new folder called quotes-scraper in your project directory and initialize it with a package.json file:

bash
$ mkdir quotes-scraper $ cd quotes-scraper $ npm init -y

Afterward, create an app.js file in the root of the quotes-scraper directory and populate it with the following content:

javascript
const axios = require("axios"); const cheerio = require("cheerio"); const url = "http://quotes.toscrape.com/"; exports.lambdaHandler = async (_event, _context) => { try { const response = await axios(url); const $ = cheerio.load(response.data); const container = $(".container .quote"); const quotes = []; container.each(function () { const text = $(this).find(".text").text(); const author = $(this).find(".author").text(); const tags = $(this).find(".tag"); const tagArray = []; tags.each(function () { const tagText = $(this).text(); tagArray.push(tagText); }); quotes.push({ text, author, tag: tagArray, }); }); return { statusCode: 200, body: JSON.stringify(quotes), }; } catch (err) { console.log(err); throw err; } };

This code scrapes the quotes on this website and returns them as a JSON object. It uses axios to fetch the HTML and cheerio to extract the relevant parts. Ensure you install both dependencies in the quotes-scraper directory:

bash
$ npm install axios cheerio

Afterward, open the template.yml file in your project root and add the following code to the Resources section:

yaml
Resources: . . . QuotesScraperFunction: Type: AWS::Serverless::Function Properties: CodeUri: quotes-scraper/ Handler: app.lambdaHandler Runtime: nodejs18.x Architectures: - x86_64 Events: QuotesScraper: Type: Api Properties: Path: /quotes Method: get

Next, add the following snippet to the Output section:

yaml
Output: . . . QuotesScraperApi: Description: "API Gateway endpoint URL for Prod stage for Quotes Scraper function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/quotes/" QuotesScraperFunction: Description: "Quotes Scraper Function ARN" Value: !GetAtt QuotesScraperFunction.Arn QuotesScraperFunctionIamRole: Description: "Implicit IAM Role created for Quotes Scraper function" Value: !GetAtt QuotesScraperFunctionRole.Arn

Save and close the file, then invoke your new function through the SAM CLI:

bash
$ sam build $ sam local invoke "QuotesScraperFunction" | jq [ { text: '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', author: 'Albert Einstein', tag: [ 'change', 'deep-thoughts', 'thinking', 'world' ] }, { text: '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', author: 'J.K. Rowling', tag: [ 'abilities', 'choices' ] }, . . . ]

Go ahead and deploy your function with the sam deploy command. Afterward, you will be able to invoke the function through the API Gateway endpoint:

plaintext
https://<API_ID>.execute-api.<AWS_REGION>.amazonaws.com/Prod/quotes/

Creating Serverless APIs with AWS Lambda and Node.js: Wrap-Up and Next Steps

I hope this article has helped you learn the basics of building serverless APIs with the help of AWS Lambda and Node.js.

There are so many other topics concerning Lambda functions that we've not covered here, including authentication, logging and monitoring, caching, persisting to a database, and more.

To build on the knowledge you've gained through this tutorial, read up on those topics and check out some of the recommended practices for working effectively with AWS Lambda functions.

Thanks for reading, and 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.

Ayooluwa Isaiah

Ayooluwa Isaiah

Ayo is a Software Developer by trade. He enjoys writing about diverse technologies in web development, mainly in Go and JavaScript/TypeScript.

All articles by Ayooluwa Isaiah

Become our next author!

Find out more

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