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.0If 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 ItemsTableA 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/prodHere 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: 5Monitoring: 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.