Deploy AWS Lambda Functions with Claude Code

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Problem

Deploying AWS Lambda functions involves writing the function code, configuring IAM roles, setting up API Gateway triggers, managing environment variables, handling cold starts, packaging dependencies, and coordinating deployments across multiple functions. The AWS console is tedious, and Infrastructure as Code templates have a steep learning curve.

Quick Start

Ask Claude Code to scaffold a Lambda function with SAM:

Create an AWS Lambda function that:
- Handles POST /api/webhooks/stripe for Stripe webhook processing
- Validates the Stripe signature
- Processes payment events (payment_intent.succeeded, charge.refunded)
- Stores events in DynamoDB
- Uses SAM for deployment
- Includes proper IAM permissions (least privilege)
- Has a local testing setup

What’s Happening

AWS Lambda runs your code without provisioning servers. You upload a function, configure triggers (API Gateway, SQS, S3 events, etc.), and AWS handles scaling, availability, and infrastructure. The Serverless Application Model (SAM) is an extension of CloudFormation that simplifies Lambda deployment.

Claude Code handles the full Lambda development workflow: writing the function, creating the SAM template, configuring permissions, setting up local testing, and preparing the CI/CD pipeline.

Step-by-Step Guide

Step 1: Set up the project structure

Ask Claude Code to create the project:

Set up a SAM project for a webhook processing service with:
- TypeScript Lambda functions
- DynamoDB table for event storage
- API Gateway with custom domain
- Local development with SAM CLI
my-webhook-service/
├── src/
│   ├── handlers/
│   │   ├── stripeWebhook.ts
│   │   └── processEvent.ts
│   ├── lib/
│   │   ├── stripe.ts
│   │   └── dynamodb.ts
│   └── types/
│       └── events.ts
├── tests/
│   ├── unit/
│   │   └── stripeWebhook.test.ts
│   └── events/
│       └── stripe-webhook.json
├── template.yaml
├── samconfig.toml
├── tsconfig.json
├── package.json
└── esbuild.config.ts

Step 2: Write the Lambda function

// src/handlers/stripeWebhook.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import Stripe from 'stripe';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
});

const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE_NAME = process.env.EVENTS_TABLE!;

export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
  // Validate Stripe signature
  const signature = event.headers['stripe-signature'];
  if (!signature || !event.body) {
    return { statusCode: 400, body: JSON.stringify({ error: 'Missing signature or body' }) };
  }

  let stripeEvent: Stripe.Event;
  try {
    stripeEvent = stripe.webhooks.constructEvent(
      event.body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch (err) {
    console.error('Signature verification failed:', err);
    return { statusCode: 400, body: JSON.stringify({ error: 'Invalid signature' }) };
  }

  // Store event for idempotency and audit
  try {
    await dynamodb.send(new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        pk: stripeEvent.id,
        sk: stripeEvent.type,
        data: stripeEvent.data.object,
        createdAt: new Date().toISOString(),
        processed: false,
      },
      ConditionExpression: 'attribute_not_exists(pk)', // Idempotency
    }));
  } catch (err: unknown) {
    if ((err as { name: string }).name === 'ConditionalCheckFailedException') {
      // Already processed, return success
      return { statusCode: 200, body: JSON.stringify({ received: true, duplicate: true }) };
    }
    throw err;
  }

  // Process based on event type
  switch (stripeEvent.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(stripeEvent.data.object as Stripe.PaymentIntent);
      break;
    case 'charge.refunded':
      await handleRefund(stripeEvent.data.object as Stripe.Charge);
      break;
    default:
      console.log(`Unhandled event type: ${stripeEvent.type}`);
  }

  return { statusCode: 200, body: JSON.stringify({ received: true }) };
}

async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent): Promise<void> {
  console.log(`Payment succeeded: ${paymentIntent.id}, amount: ${paymentIntent.amount}`);
  // Business logic here
}

async function handleRefund(charge: Stripe.Charge): Promise<void> {
  console.log(`Charge refunded: ${charge.id}, amount refunded: ${charge.amount_refunded}`);
  // Business logic here
}

Step 3: Create the SAM template

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Stripe webhook processing service

Globals:
  Function:
    Timeout: 30
    MemorySize: 256
    Runtime: nodejs20.x
    Architectures:
      - arm64  # Graviton2 - cheaper and faster
    Environment:
      Variables:
        EVENTS_TABLE: !Ref EventsTable

Parameters:
  Stage:
    Type: String
    Default: dev
    AllowedValues: [dev, staging, prod]

Resources:
  StripeWebhookFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handlers/stripeWebhook.handler
      Description: Processes Stripe webhook events
      Environment:
        Variables:
          STRIPE_SECRET_KEY: !Sub '{{resolve:secretsmanager:${Stage}/stripe:SecretString:secret_key}}'
          STRIPE_WEBHOOK_SECRET: !Sub '{{resolve:secretsmanager:${Stage}/stripe:SecretString:webhook_secret}}'
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref EventsTable
      Events:
        StripeWebhook:
          Type: Api
          Properties:
            Path: /api/webhooks/stripe
            Method: POST
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        EntryPoints:
          - src/handlers/stripeWebhook.ts

  EventsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub '${Stage}-webhook-events'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true

Outputs:
  WebhookUrl:
    Description: Stripe webhook URL
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api/webhooks/stripe'

Step 4: Optimize for cold starts

Ask Claude Code to reduce cold start latency:

Optimize my Lambda function for minimal cold start time:
- Minimize bundle size with tree-shaking
- Move initialization outside the handler
- Use the AWS SDK v3 (modular imports)
- Consider Lambda SnapStart or provisioned concurrency
// Move SDK initialization outside the handler (reused across invocations)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

// These are created once during cold start and reused
const client = new DynamoDBClient({});
const dynamodb = DynamoDBDocumentClient.from(client);

// Import only what you need from the SDK (tree-shaking)
// Good: import { PutCommand } from '@aws-sdk/lib-dynamodb'
// Bad: import AWS from 'aws-sdk' (imports everything)

Step 5: Test locally

# Start local API Gateway + Lambda
sam local start-api

# Invoke with a test event
sam local invoke StripeWebhookFunction -e tests/events/stripe-webhook.json

# Run unit tests
npm test

Create test events:

{
  "httpMethod": "POST",
  "path": "/api/webhooks/stripe",
  "headers": {
    "stripe-signature": "t=1234567890,v1=test-signature"
  },
  "body": "{\"id\":\"evt_test\",\"type\":\"payment_intent.succeeded\",\"data\":{\"object\":{\"id\":\"pi_test\",\"amount\":2000}}}"
}

Step 6: Set up deployment pipeline

# .github/workflows/deploy.yml
name: Deploy Lambda
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci

      - run: npm test

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: us-east-1

      - uses: aws-actions/setup-sam@v2

      - run: sam build

      - run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --parameter-overrides Stage=prod

Step 7: Add monitoring and alerting

Ask Claude Code to add observability:

Add CloudWatch alarms for:
- Function errors > 1% of invocations
- P99 latency > 5 seconds
- Throttling events
- DynamoDB capacity consumption
Include structured logging with correlation IDs.
# Add to template.yaml
  ErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub '${Stage}-webhook-errors'
      MetricName: Errors
      Namespace: AWS/Lambda
      Dimensions:
        - Name: FunctionName
          Value: !Ref StripeWebhookFunction
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 5
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref AlertTopic

Prevention

Add Lambda development rules to your CLAUDE.md:

## AWS Lambda Rules
- Use ARM64 (Graviton) for all functions
- Keep handlers thin — delegate to service modules
- Initialize SDK clients outside the handler
- Use AWS SDK v3 with modular imports
- Store secrets in Secrets Manager, never in environment variables
- Set function timeout to 2x expected execution time
- Use DynamoDB condition expressions for idempotency
- Every function must have error alarms

**Written by Michael** — solo dev, Da Nang, Vietnam. 50K+ Chrome extension users. $500K+ on Upwork (100% Job Success). Runs 5 Claude Max subs in parallel. Built this site with autonomous agent fleets. [See what I'm building →](https://zovo.one)

This took me 3 hours to figure out. I put it in a CLAUDE.md so I'd never figure it out again. Now Claude gets it right on the first try, every project. 16 framework templates. Next.js, FastAPI, Laravel, Rails, Go, Rust, Terraform, and 9 more. Each one 300+ lines of "here's exactly how this stack works." Copy into your project. Done. **[See the templates →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-deploy&utm_campaign=claude-code-aws-lambda-deployment-guide)** $99 once. Yours forever. I keep adding templates monthly.