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!

Similar Posts