How to Build a Serverless API with AWS Lambda and Node.js
Serverless computing is revolutionizing how we build and deploy web applications. With serverless, you can focus on writing code without worrying about managing servers, scaling infrastructure, or paying for idle resources.
In this in-depth guide, we‘ll walk through how to build a RESTful API with AWS Lambda, Node.js, and other serverless technologies. We‘ll cover the core concepts, provide detailed code samples, and share expert tips to help you get started.
What is Serverless Architecture?
Serverless is an approach to building cloud-based applications where the infrastructure is fully managed by the cloud provider. You write your application code as individual "functions" that are triggered by events like HTTP requests or database changes. The cloud platform dynamically allocates resources to execute the functions and automatically scales based on the workload.
The key characteristics of serverless are:
- No servers to provision or manage
- Pay only for resources consumed, not idle time
- Built-in auto-scaling and high availability
- Enables faster development velocity
Popular serverless platforms include AWS Lambda, Azure Functions, Google Cloud Functions, and IBM Cloud Functions. For this tutorial, we‘ll be using AWS.
Overview of AWS Serverless Services
To build our serverless API, we‘ll be using the following AWS services:
AWS Lambda
Lambda is the core compute service for running code without provisioning servers. You upload your code and Lambda takes care of everything required to execute it. Lambda supports multiple languages including Node.js, Python, Java, and C#.
API Gateway
API Gateway is a fully managed service for creating, publishing, and securing APIs at any scale. It provides a RESTful API interface for invoking Lambda functions and integrates with other AWS services.
DynamoDB
DynamoDB is a NoSQL database that provides single-digit millisecond performance at any scale. It‘s fully managed, supports document and key-value models, and is a great fit for serverless applications.
CloudFormation
CloudFormation allows you to model your application resources as code. You define your resources in a template file (JSON or YAML) and CloudFormation takes care of provisioning and configuring those resources for you.
Building the Serverless API
Now that we understand the core services, let‘s dive into building the API. We‘ll create a simple API for a task list application that allows creating and retrieving tasks.
Step 1: Define the API with Swagger
First, we‘ll define the API interface using the Swagger specification. Swagger allows you to describe your API in a language-agnostic way using a simple YAML or JSON file.
swagger: "2.0"
info:
title: Task List API
version: 1.0.0
description: A simple API for a task list application
paths:
/tasks:
post:
summary: Create a task
parameters:
- name: task
in: body
schema:
$ref: ‘#/definitions/Task‘
responses:
201:
description: Task created
get:
summary: List all tasks
responses:
200:
description: OK
schema:
type: array
items:
$ref: ‘#/definitions/Task‘
definitions:
Task:
type: object
properties:
id:
type: string
name:
type: string
completed:
type: boolean
This defines a simple API with two endpoints:
- POST /tasks to create a new task
- GET /tasks to retrieve all tasks
Step 2: Implement the Lambda Function
Next, we‘ll write the code for the Lambda function that implements the API logic. We‘ll use Node.js and the Serverless Framework to make development easier.
const AWS = require(‘aws-sdk‘);
const dynamo = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.TASKS_TABLE;
exports.handler = async (event, context) => {
let response;
try {
switch (event.httpMethod) {
case ‘GET‘:
response = await dynamo.scan({ TableName: tableName }).promise();
break;
case ‘POST‘: {
const task = JSON.parse(event.body);
task.id = context.awsRequestId;
await dynamo.put({
TableName: tableName,
Item: task
}).promise();
response = {
statusCode: 201,
body: JSON.stringify(task)
};
break;
}
default:
throw new Error(`Unsupported method "${event.httpMethod}"`);
}
return {
statusCode: 200,
body: JSON.stringify(response)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
This Lambda function handles the two API endpoints. For GET requests, it retrieves all tasks from the DynamoDB table. For POST requests, it creates a new task and stores it in the database.
A few key things to note:
- We use the
aws-sdk
to interact with DynamoDB. The SDK is automatically available in the Lambda runtime. - The DynamoDB table name is read from an environment variable that we‘ll set later.
- The function is async and uses the Node.js 8.10 runtime, which makes development easier.
Step 3: Configure the Serverless Application
Now we need to configure the Lambda function, the API Gateway, and provision the DynamoDB table. We‘ll use the Serverless Framework and CloudFormation to automate this.
Create a serverless.yml
file with the following:
service: task-list-api
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
environment:
TASKS_TABLE: ${self:service}-${self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.TASKS_TABLE}"
functions:
api:
handler: handler.handler
events:
- http: ANY /
- http: ANY /{proxy+}
resources:
Resources:
TasksTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.TASKS_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
This configuration file tells the Serverless Framework to:
- Create a Lambda function called
api
that runs our handler code - Configure the function to be triggered by ANY requests to the API Gateway (HTTP proxy)
- Set an environment variable for the DynamoDB table name
- Provision an IAM role for the Lambda function with permissions to access DynamoDB
- Provision a DynamoDB table via CloudFormation to store the tasks
Step 4: Deploy the API
We‘re ready to deploy our serverless API! First, make sure you have the Serverless Framework installed:
npm install -g serverless
Then deploy the application:
serverless deploy
The Serverless Framework will package your code, create the AWS resources, and deploy your application. Once it‘s done, it will print out the URL for your API endpoint.
Step 5: Test the API
Let‘s test out the API! You can use curl or a tool like Postman to send HTTP requests.
To create a task:
curl -X POST https://{api-id}.execute-api.us-east-1.amazonaws.com/dev/tasks \
-d ‘{"name":"My first task","completed":false}‘
To list all tasks:
curl https://{api-id}.execute-api.us-east-1.amazonaws.com/dev/tasks
And that‘s it! With just a few lines of code and some configuration, we have a fully functional serverless API.
Best Practices for Serverless APIs
Here are some tips and best practices to keep in mind when building serverless APIs:
- Keep your Lambda functions small and focused. Separate concerns into individual functions for better scalability and organization.
- Use environment variables for config and secrets. Don‘t hardcode sensitive values.
- Leverage serverless plugins to automate common tasks like creating domains, seeding databases, etc.
- Monitor and log your application. Use AWS X-Ray for tracing and CloudWatch for logs and metrics.
- Secure your APIs. Use API keys, IAM permissions, and OAuth or JWT tokens for authentication and authorization.
- Test locally and practice CI/CD. The Serverless Framework provides tools for running functions locally and deploying via your CI service.
Serverless Limitations and Tradeoffs
While serverless has many benefits, it‘s important to understand the limitations and tradeoffs:
- Cold starts can introduce latency, especially for infrequently used functions.
- Functions are stateless and have limited runtime durations (e.g. 15 minutes on AWS Lambda), so they aren‘t suitable for long-running processes.
- Vendor lock-in is a risk since your application is tightly coupled to the platform‘s proprietary services.
- Monitoring and debugging can be challenging without direct access to servers. Distributed tracing is essential.
- There are some cost advantages to serverless, but it isn‘t always cheaper. Depending on your traffic patterns, a traditional server/container model might be more cost-effective.
Conclusion
Serverless is a powerful approach for building scalable, cost-effective applications without managing infrastructure. Serverless APIs are particularly compelling for their simplicity and flexibility.
In this guide, we covered how to build a serverless API with Node.js and popular AWS services like Lambda, API Gateway, and DynamoDB. With the Serverless Framework and a little bit of code, you can create a production-ready API in minutes.
Of course, serverless isn‘t a silver bullet. It‘s important to understand the benefits and limitations. When applied correctly, though, serverless can dramatically improve the agility and efficiency of your development process.
Looking to get started? Check out the code samples from this guide and give the Serverless Framework a try. Happy building!