Back
AWS

Lambda and API Gateway: Serverless Architecture on AWS

Master serverless architecture on AWS with Lambda and API Gateway. Learn how to create Lambda functions in Node.js, configure REST and HTTP API Gateway, integrate DynamoDB, handle environment variables, layers, cold starts, and deploy with SAM and Serverless Framework.

Francisco ZapataWritten by Francisco Zapata
February 2, 202612 min read
Lambda and API Gateway: Serverless Architecture on AWS

Serverless architecture has transformed how we build applications in the cloud. With AWS Lambda you run code without provisioning or managing servers, and with API Gateway you expose that code as REST or HTTP APIs accessible from any client. In this guide, you will learn how to build a complete serverless API with Node.js, DynamoDB as the database, and production-ready best practices.

What Is AWS Lambda?

Lambda is a serverless compute service that runs your code in response to events. You only pay for the compute time you consume, billed per millisecond of execution. Lambda scales automatically from zero to thousands of concurrent executions without any configuration on your part.

Key Characteristics:

  • Supported runtimes: Node.js, Python, Java, Go, .NET, Ruby, and custom runtimes
  • Configurable memory: From 128 MB to 10,240 MB
  • Maximum timeout: 15 minutes per invocation
  • Temporary storage: 512 MB to 10 GB at /tmp
  • Deployment package: Up to 50 MB (ZIP) or 250 MB uncompressed

Your First Lambda Function

Let's create a Lambda function in Node.js that handles HTTP requests:

// handler.js
export const handler = async (event) => {
  const { httpMethod, path, body, queryStringParameters } = event;

  try {
    switch (httpMethod) {
      case 'GET':
        return {
          statusCode: 200,
          headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
          },
          body: JSON.stringify({
            message: 'Products retrieved successfully',
            data: await getProducts(queryStringParameters)
          })
        };

      case 'POST':
        const newProduct = JSON.parse(body);
        return {
          statusCode: 201,
          headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
          },
          body: JSON.stringify({
            message: 'Product created successfully',
            data: await createProduct(newProduct)
          })
        };

      default:
        return {
          statusCode: 405,
          body: JSON.stringify({ message: 'Method not allowed' })
        };
    }
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal server error' })
    };
  }
};

Creating the Function with AWS CLI

# Zip the code
zip -r function.zip handler.js node_modules/

# Create the Lambda function
aws lambda create-function \
  --function-name my-products-api \
  --runtime nodejs20.x \
  --handler handler.handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::123456789012:role/lambda-execution-role \
  --timeout 30 \
  --memory-size 256 \
  --environment Variables='{
    "TABLE_NAME": "products",
    "REGION": "us-east-1"
  }'

DynamoDB Integration

DynamoDB is the natural choice for databases in serverless architectures. It is a fully managed NoSQL database that scales automatically:

// dynamodb.js
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  ScanCommand,
  DeleteCommand
} from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({ region: process.env.REGION });
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME;

export async function getProducts(params) {
  const command = new ScanCommand({
    TableName: TABLE_NAME,
    Limit: params?.limit ? parseInt(params.limit) : 50
  });
  const result = await docClient.send(command);
  return result.Items;
}

export async function createProduct(product) {
  const item = {
    id: crypto.randomUUID(),
    ...product,
    createdAt: new Date().toISOString()
  };
  const command = new PutCommand({
    TableName: TABLE_NAME,
    Item: item
  });
  await docClient.send(command);
  return item;
}

export async function getProductById(id) {
  const command = new GetCommand({
    TableName: TABLE_NAME,
    Key: { id }
  });
  const result = await docClient.send(command);
  return result.Item;
}

API Gateway: REST vs HTTP API

API Gateway offers two API types. Choosing the right one can impact both functionality and costs:

| Feature | REST API | HTTP API |

|---|---|---|

| Pricing | $3.50/million requests | $1.00/million requests |

| Latency | Higher (~10-15ms overhead) | Lower (~5ms overhead) |

| Authorization | IAM, Cognito, Lambda Auth | IAM, Cognito, JWT |

| Caching | Yes (built-in) | No |

| Transformations | Yes (mapping templates) | No |

| WebSocket | Yes | No |

| Recommended for | Complex APIs | Simple APIs, microservices |

For most use cases, HTTP API is sufficient and more cost-effective.

Creating an HTTP API with API Gateway

# Create the HTTP API
aws apigatewayv2 create-api \
  --name my-products-api \
  --protocol-type HTTP \
  --target arn:aws:lambda:us-east-1:123456789012:function:my-products-api

# Create routes
aws apigatewayv2 create-route \
  --api-id abc123def \
  --route-key "GET /products"

aws apigatewayv2 create-route \
  --api-id abc123def \
  --route-key "POST /products"

aws apigatewayv2 create-route \
  --api-id abc123def \
  --route-key "GET /products/{id}"

# Grant API Gateway permission to invoke Lambda
aws lambda add-permission \
  --function-name my-products-api \
  --statement-id apigateway-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123def/*/*"

Lambda Layers

Layers let you share code and dependencies across multiple Lambda functions:

# Create the layer structure
mkdir -p nodejs/node_modules
cd nodejs
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
cd ..
zip -r dynamodb-layer.zip nodejs/

# Publish the layer
aws lambda publish-layer-version \
  --layer-name dynamodb-utils \
  --zip-file fileb://dynamodb-layer.zip \
  --compatible-runtimes nodejs20.x nodejs22.x

# Attach the layer to the function
aws lambda update-function-configuration \
  --function-name my-products-api \
  --layers arn:aws:lambda:us-east-1:123456789012:layer:dynamodb-utils:1

Environment Variables and Secrets

# Update environment variables
aws lambda update-function-configuration \
  --function-name my-products-api \
  --environment Variables='{
    "TABLE_NAME": "products",
    "REGION": "us-east-1",
    "STAGE": "production",
    "LOG_LEVEL": "info"
  }'

For sensitive secrets, use AWS Secrets Manager or SSM Parameter Store instead of environment variables.

Cold Starts: What They Are and How to Mitigate Them

A cold start happens when Lambda must initialize a new execution environment. This adds extra latency (typically between 100ms and 1s for Node.js). Strategies to reduce them:

1. Provisioned Concurrency: Keeps pre-warmed instances ready

2. Lambda SnapStart: Reduces cold starts to sub-second (available for Java and Node.js)

3. Keep functions small: Fewer dependencies mean faster startup

4. Initialize connections outside the handler: They get reused across invocations

# Configure Provisioned Concurrency
aws lambda put-provisioned-concurrency-config \
  --function-name my-products-api \
  --qualifier production \
  --provisioned-concurrent-executions 5

Deploying with SAM (Serverless Application Model)

AWS SAM streamlines the deployment of serverless applications with a declarative template:

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Products API

Globals:
  Function:
    Runtime: nodejs20.x
    Timeout: 30
    MemorySize: 256
    Environment:
      Variables:
        TABLE_NAME: !Ref ProductsTable
        REGION: !Ref AWS::Region

Resources:
  ProductsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      CodeUri: ./src
      Events:
        GetProducts:
          Type: HttpApi
          Properties:
            Path: /products
            Method: GET
        CreateProduct:
          Type: HttpApi
          Properties:
            Path: /products
            Method: POST
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductsTable

  ProductsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: products
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH

Outputs:
  ApiUrl:
    Description: API URL
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
# Build and deploy with SAM
sam build
sam deploy --guided

# For subsequent deployments
sam deploy

Lambda Pricing

Lambda offers a generous free tier that never expires:

  • 1 million free requests per month
  • 400,000 GB-seconds of compute per month
  • After that: $0.20 per million requests + $0.0000166667 per GB-second

For a function with 256 MB of memory running 200ms per invocation, you can make roughly 3.2 million free invocations per month.

Conclusion

Lambda and API Gateway enable you to build scalable APIs without managing servers. Combined with DynamoDB, you have a fully serverless architecture that scales automatically and charges only for what you use. SAM simplifies deployment and infrastructure-as-code management.

Comments (0)

Leave a comment

Be the first to comment