ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-03-28Intermediate

Building Serverless APIs with Antigravity × AWS Lambda — DynamoDB, API Gateway & S3 Integration

Learn how to build scalable serverless REST APIs on AWS Lambda using Antigravity's AI agents. Covers DynamoDB data persistence, API Gateway routing, S3 file uploads, and Infrastructure as Code with SAM.

antigravity435aws-lambdaserverless3dynamodbapi-gateways3tutorial6

Setup and context — Why Antigravity × AWS Lambda?

Serverless architecture has become a go-to approach for building scalable APIs without the overhead of managing infrastructure. AWS Lambda, as the industry-leading event-driven compute service, powers countless production workloads worldwide.

However, building a complete serverless backend on AWS involves a surprising amount of configuration: writing Lambda functions, setting up API Gateway routes, defining DynamoDB tables, configuring IAM policies, and managing S3 buckets. This is where Antigravity's AI agents truly shine. By describing what you need in natural language, the agent generates boilerplate code, infrastructure definitions, and integration logic in one cohesive workflow.

In this guide, we'll walk through building a fully functional serverless REST API using Antigravity, covering everything from project scaffolding to production deployment. You'll learn how to integrate DynamoDB for data persistence, API Gateway for endpoint routing, and S3 for file storage — all powered by Antigravity's intelligent code generation.

Who this article is for:

  • Developers familiar with basic AWS services (Lambda, DynamoDB, S3)
  • Anyone looking to accelerate backend development with Antigravity
  • Intermediate developers interested in serverless architecture

Setting Up Your Environment

Before diving in, make sure your development environment is ready for AWS serverless development with Antigravity.

Required tools:

  • Antigravity (latest version recommended)
  • AWS CLI v2 (authenticated via aws configure)
  • Node.js 20 LTS or later
  • AWS SAM CLI or Serverless Framework

Run the following commands in Antigravity's terminal to verify your setup:

# Verify AWS CLI authentication
aws sts get-caller-identity
 
# Expected output:
# {
#   "UserId": "AIDACKCEVSQ6C2EXAMPLE",
#   "Account": "123456789012",
#   "Arn": "arn:aws:iam::123456789012:user/dev-user"
# }
 
# Check SAM CLI version
sam --version
# SAM CLI, version 1.120.0

If you haven't configured AWS CLI yet, run aws configure to set your access key and preferred region. This guide uses ap-northeast-1 (Tokyo), but you can substitute your preferred region.

Project Scaffolding with Antigravity

Start by asking Antigravity's agent to generate the project structure. In the agent chat, provide a prompt like this:

Initialize an AWS SAM project for a REST API using DynamoDB
as the data store. Use Node.js 20 runtime with TypeScript.
Create CRUD endpoints (GET/POST/PUT/DELETE) for an items resource.

Antigravity's agent will generate a template.yaml (SAM template) along with Lambda function code. Here's the resulting project structure:

serverless-api/
├── template.yaml           # SAM template (infrastructure definition)
├── src/
│   ├── handlers/
│   │   ├── createItem.ts   # POST /items
│   │   ├── getItem.ts      # GET /items/{id}
│   │   ├── listItems.ts    # GET /items
│   │   ├── updateItem.ts   # PUT /items/{id}
│   │   └── deleteItem.ts   # DELETE /items/{id}
│   ├── lib/
│   │   ├── dynamodb.ts     # DynamoDB client helper
│   │   └── response.ts     # API response utilities
│   └── types/
│       └── item.ts         # Type definitions
├── tsconfig.json
├── package.json
└── samconfig.toml

Understanding the SAM Template — Infrastructure as Code

The template.yaml file defines your AWS infrastructure as code. Let's examine the key sections generated by Antigravity.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless REST API with DynamoDB
 
Globals:
  Function:
    Timeout: 15
    Runtime: nodejs20.x
    MemorySize: 256
    Environment:
      Variables:
        TABLE_NAME: !Ref ItemsTable
        BUCKET_NAME: !Ref AssetsBucket
 
Resources:
  # API Gateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      Cors:
        AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization'"
        AllowOrigin: "'*'"
 
  # DynamoDB Table
  ItemsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: serverless-items
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
 
  # S3 Bucket (for file uploads)
  AssetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${AWS::StackName}-assets"
      CorsConfiguration:
        CorsRules:
          - AllowedMethods: [GET, PUT]
            AllowedOrigins: ['*']
            AllowedHeaders: ['*']
 
  # Lambda Function (Create)
  CreateItemFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/createItem.handler
      Events:
        CreateItem:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /items
            Method: post
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ItemsTable
 
  # Lambda Function (List)
  ListItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/listItems.handler
      Events:
        ListItems:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /items
            Method: get
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ItemsTable

A key design decision here is using BillingMode: PAY_PER_REQUEST (on-demand pricing) for DynamoDB. This is more cost-effective during early development and for workloads with unpredictable traffic patterns compared to provisioned capacity mode.

Implementing Lambda Functions in TypeScript

Now let's look at the actual Lambda function implementations. We'll start with the DynamoDB client helper module.

// src/lib/dynamodb.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
  ScanCommand,
  UpdateCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
 
// Initialize DynamoDB Document Client
const client = new DynamoDBClient({ region: "ap-northeast-1" });
const docClient = DynamoDBDocumentClient.from(client);
 
const TABLE_NAME = process.env.TABLE_NAME || "serverless-items";
 
// Create an item
export async function createItem(item: Record<string, unknown>) {
  await docClient.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: item,
    })
  );
  return item;
}
 
// Get an item by ID
export async function getItemById(id: string) {
  const result = await docClient.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  return result.Item;
}
 
// List all items with pagination support
export async function listAllItems(limit = 20, lastKey?: string) {
  const params: Record<string, unknown> = {
    TableName: TABLE_NAME,
    Limit: limit,
  };
  if (lastKey) {
    params.ExclusiveStartKey = { id: lastKey };
  }
  const result = await docClient.send(new ScanCommand(params));
  return {
    items: result.Items || [],
    nextKey: result.LastEvaluatedKey?.id,
  };
}
 
// Delete an item by ID
export async function deleteItemById(id: string) {
  await docClient.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
}

Next, the POST endpoint handler:

// src/handlers/createItem.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { randomUUID } from "crypto";
import { createItem } from "../lib/dynamodb";
import { success, error } from "../lib/response";
 
export async function handler(
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
  try {
    // Parse request body
    const body = JSON.parse(event.body || "{}");
 
    // Validation
    if (!body.name || !body.description) {
      return error(400, "name and description are required");
    }
 
    // Create the item
    const item = {
      id: randomUUID(),
      name: body.name,
      description: body.description,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };
 
    const created = await createItem(item);
 
    // Return 201 Created
    return success(201, created);
  } catch (err) {
    console.error("CreateItem error:", err);
    return error(500, "Failed to create item");
  }
}

The response utility centralizes CORS header management — a common source of bugs in serverless APIs:

// src/lib/response.ts
const headers = {
  "Content-Type": "application/json",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
};
 
export function success(statusCode: number, body: unknown) {
  return {
    statusCode,
    headers,
    body: JSON.stringify(body),
  };
}
 
export function error(statusCode: number, message: string) {
  return {
    statusCode,
    headers,
    body: JSON.stringify({ error: message }),
  };
}

S3 Integration — File Uploads with Presigned URLs

When implementing file uploads in a serverless API, the best practice is to use S3 presigned URLs rather than routing files through Lambda. This bypasses Lambda's 6MB payload limit and handles large files efficiently.

// src/handlers/getUploadUrl.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { randomUUID } from "crypto";
import { success, error } from "../lib/response";
 
const s3 = new S3Client({ region: "ap-northeast-1" });
const BUCKET = process.env.BUCKET_NAME!;
 
export async function handler(
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
  try {
    const body = JSON.parse(event.body || "{}");
    const contentType = body.contentType || "application/octet-stream";
    const key = `uploads/${randomUUID()}`;
 
    // Generate presigned URL (expires in 15 minutes)
    const command = new PutObjectCommand({
      Bucket: BUCKET,
      Key: key,
      ContentType: contentType,
    });
    const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 900 });
 
    return success(200, {
      uploadUrl,    // Client uploads directly to this URL via PUT
      key,          // Object key after upload
      expiresIn: 900,
    });
  } catch (err) {
    console.error("GetUploadUrl error:", err);
    return error(500, "Failed to generate upload URL");
  }
}

On the client side, the flow is straightforward — fetch the presigned URL, then upload directly to S3:

// Client-side implementation example
async function uploadFile(file: File) {
  // 1. Get presigned URL from the API
  const res = await fetch("/items/upload-url", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ contentType: file.type }),
  });
  const { uploadUrl, key } = await res.json();
 
  // 2. Upload directly to S3
  await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });
 
  return key; // Store this in DynamoDB to associate with a record
}

Local Development and Debugging with SAM CLI

Testing locally before deploying is essential in serverless development. AWS SAM CLI lets you run Lambda functions on your machine.

# Start API Gateway + Lambda locally
sam local start-api --warm-containers EAGER
 
# Output:
# Mounting CreateItemFunction at http://127.0.0.1:3000/items [POST]
# Mounting ListItemsFunction at http://127.0.0.1:3000/items [GET]
# ...
 
# Send a test request from another terminal
curl -X POST http://127.0.0.1:3000/items \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Item", "description": "SAM local test"}'
 
# Expected output:
# {
#   "id": "a1b2c3d4-...",
#   "name": "Test Item",
#   "description": "SAM local test",
#   "createdAt": "2026-03-28T12:00:00.000Z",
#   "updatedAt": "2026-03-28T12:00:00.000Z"
# }

You can also delegate debugging to Antigravity's agent. Tell it something like "My createItem Lambda is returning a 500 error — check the DynamoDB connection," and the agent will analyze logs, identify missing IAM policies or misconfigured environment variables, and suggest fixes.

Deployment and Production Best Practices

Once local testing passes, deploy to AWS:

# First deployment (guided setup)
sam deploy --guided
 
# Subsequent deployments
sam deploy
 
# After deployment, the endpoint URL is shown:
# Outputs:
# ApiUrl: https://abc123.execute-api.ap-northeast-1.amazonaws.com/prod

Here are some key production considerations.

Cold start mitigation: Lambda containers are recycled after periods of inactivity, causing latency spikes on the next invocation. For latency-sensitive APIs, configure Provisioned Concurrency or minimize initialization code in your functions.

# Provisioned Concurrency in template.yaml
CreateItemFunction:
  Type: AWS::Serverless::Function
  Properties:
    # ... existing config
    ProvisionedConcurrencyConfig:
      ProvisionedConcurrentExecutions: 5

Monitoring: CloudWatch Logs are enabled automatically, but structured logging makes debugging significantly easier. Ask Antigravity to "add structured JSON logging to the Lambda functions" and it will generate an implementation using @aws-lambda-powertools/logger.

Security: Follow the principle of least privilege for IAM policies. Each Lambda function should have only the minimum permissions it needs. SAM's policy templates like DynamoDBCrudPolicy and S3ReadPolicy make this straightforward.

For a deeper dive into API security patterns, check out Building Secure API Backends with Antigravity Agents, which covers authentication, validation, and rate limiting in detail.

Summary

The Antigravity × AWS Lambda combination dramatically accelerates serverless API development. From SAM template generation and TypeScript Lambda implementations to DynamoDB helpers and S3 presigned URL logic, Antigravity's AI agent handles the heavy lifting so you can focus on business logic rather than AWS configuration boilerplate.

If you're ready to take your serverless architecture to the next level with event-driven microservices, the Antigravity × Event-Driven Architecture Guide provides an excellent next step.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-03-25
Antigravity × Upstash Redis: Build Fast, Secure APIs with Edge Caching and Rate Limiting
Learn how to implement edge caching and rate limiting on Cloudflare Workers using Antigravity and Upstash Redis, with step-by-step code examples.
App Dev2026-03-24
How to Build Real-Time Multiplayer Apps with AI Studio, Antigravity, and Firebase
A step-by-step guide to building real-time multiplayer apps from scratch using Google AI Studio's new full-stack vibe coding features, the Antigravity agent, and Firebase.
App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →