Use Cases

Claude Code × AWS API Gateway Complete Guide | REST API Design to Deployment Automation

Automate AWS API Gateway endpoint design with Claude Code. Resource design, Lambda integration, authentication, and deployment — real-world code from Masa's professional experience.

How API Gateway Design Time Dropped to One-Third

I’m Masa, running claudecode-lab.com. When I was doing backend freelance work, AWS API Gateway design and implementation topped the list of “tedious, time-consuming tasks.” Write an endpoint spec doc, translate it into CloudFormation, manually wire up Lambda integrations, configure CORS — and make the same mistakes every single time.

The turning point was about six months ago, when I tried asking Claude Code to “design a REST API based on this use case and implement it in CDK.” The output was surprisingly production-ready, and API Gateway implementation time has dropped to one-third since then.

CORS mishaps, Lambda permission errors, the 29-second timeout trap — since Claude Code started catching these issues upfront, the number of review findings has plummeted too.

This article shares every Claude Code × API Gateway pattern I actually use in professional projects, step by step.


Why Claude Code Excels at API Gateway Design

API Gateway design has a distinctive trait: it doesn’t require deep expertise, but it involves a lot of repetitive boilerplate.

  • Endpoint naming conventions (plural nouns, hierarchical structure)
  • HTTP method selection (GET/POST/PUT/PATCH/DELETE)
  • Request/response schema definitions
  • Lambda integration path variable mapping
  • CORS header configuration (always forgetting something)
  • Stage variable management (dev/staging/prod)

These follow well-defined rules. Anyone can write them by following the docs, but asking Claude Code to “design a REST API for this use case and write it in CDK” gets you a complete implementation in minutes.

Where Claude Code really shines is design consistency checking. Ask “point out any REST principle violations in this endpoint design,” and it immediately flags idempotency issues, URL inconsistencies, and more.


Step 1: Delegating REST API Endpoint Design to Claude Code

Before writing a single line of code, let Claude Code handle the endpoint design.

Example Prompt

Create a REST API endpoint specification based on the following use cases.

Use cases:
- Task management app (users manage projects and tasks)
- User authentication required (Cognito)
- Projects can have members added
- Tasks support comments and file attachments

Output format:
- Endpoint list (method, path, description)
- Request/response JSON schemas
- Highlight which endpoints require authentication
- Flag any REST principle violations

Generated Endpoint Design

# Task Management API Endpoint Design

## Authentication
POST   /auth/login          - Login (public)
POST   /auth/refresh        - Token refresh (public)
DELETE /auth/logout         - Logout (auth required)

## Projects
GET    /projects            - List projects (auth required)
POST   /projects            - Create project (auth required)
GET    /projects/{id}       - Project detail (auth required)
PUT    /projects/{id}       - Update project (auth required)
DELETE /projects/{id}       - Delete project (auth required)

## Project Members
GET    /projects/{id}/members       - List members
POST   /projects/{id}/members       - Add member
DELETE /projects/{id}/members/{uid} - Remove member

## Tasks
GET    /projects/{id}/tasks         - List tasks
POST   /projects/{id}/tasks         - Create task
GET    /tasks/{taskId}              - Task detail
PUT    /tasks/{taskId}              - Update task
DELETE /tasks/{taskId}             - Delete task

## Comments
GET    /tasks/{taskId}/comments     - List comments
POST   /tasks/{taskId}/comments     - Create comment
DELETE /tasks/{taskId}/comments/{commentId} - Delete comment

## Attachments
POST   /tasks/{taskId}/attachments  - Get presigned URL (for direct S3 upload)
GET    /tasks/{taskId}/attachments  - List attachments
DELETE /tasks/{taskId}/attachments/{attachmentId} - Delete attachment

Use this as a starting point for discussions like “should /tasks/{taskId} access be restricted to project members?”


Step 2: Auto-Generating Lambda Integration Config

With the endpoint design in hand, generate the Lambda integration setup.

Prompt

Based on the endpoint design above, build an API Gateway REST API using AWS CDK (TypeScript).
- Lambda integration (use proxy integration)
- Create Lambda function stubs for each endpoint
- Define each endpoint explicitly (no catch-all {proxy+})
- Configure request validators at the API Gateway level

Generated CDK Stack (excerpt)

import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class TaskApiStack extends cdk.Stack {
  public readonly api: apigateway.RestApi;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ===== Lambda function factory =====
    const createLambda = (name: string, handler: string) =>
      new nodejs.NodejsFunction(this, name, {
        entry: `src/handlers/${handler}.ts`,
        handler: "handler",
        runtime: lambda.Runtime.NODEJS_20_X,
        environment: {
          STAGE: props?.env?.region ?? "dev",
        },
        timeout: cdk.Duration.seconds(29), // API Gateway max timeout
        memorySize: 256,
        bundling: {
          minify: true,
          sourceMap: true,
          externalModules: ["@aws-sdk/*"],
        },
      });

    // Lambda functions
    const listProjectsFn = createLambda("ListProjects", "projects/list");
    const createProjectFn = createLambda("CreateProject", "projects/create");
    const getProjectFn = createLambda("GetProject", "projects/get");
    const updateProjectFn = createLambda("UpdateProject", "projects/update");
    const deleteProjectFn = createLambda("DeleteProject", "projects/delete");
    const listTasksFn = createLambda("ListTasks", "tasks/list");
    const createTaskFn = createLambda("CreateTask", "tasks/create");
    const getTaskFn = createLambda("GetTask", "tasks/get");
    const updateTaskFn = createLambda("UpdateTask", "tasks/update");
    const deleteTaskFn = createLambda("DeleteTask", "tasks/delete");

    // ===== REST API definition =====
    this.api = new apigateway.RestApi(this, "TaskApi", {
      restApiName: "task-management-api",
      description: "Task Management REST API",
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS, // restrict in production
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: [
          "Content-Type",
          "Authorization",
          "X-Api-Key",
        ],
      },
      defaultMethodOptions: {
        requestValidatorOptions: {
          validateRequestBody: true,
          validateRequestParameters: true,
        },
      },
      deployOptions: {
        stageName: "v1",
        accessLogDestination: new apigateway.LogGroupLogDestination(
          new cdk.aws_logs.LogGroup(this, "ApiAccessLog", {
            retention: cdk.aws_logs.RetentionDays.ONE_MONTH,
          })
        ),
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: false, // keep false in production
      },
    });

    // ===== Resources & methods =====
    const projects = this.api.root.addResource("projects");
    projects.addMethod("GET", new apigateway.LambdaIntegration(listProjectsFn));
    projects.addMethod("POST", new apigateway.LambdaIntegration(createProjectFn));

    const project = projects.addResource("{id}");
    project.addMethod("GET", new apigateway.LambdaIntegration(getProjectFn));
    project.addMethod("PUT", new apigateway.LambdaIntegration(updateProjectFn));
    project.addMethod("DELETE", new apigateway.LambdaIntegration(deleteProjectFn));

    const projectTasks = project.addResource("tasks");
    projectTasks.addMethod("GET", new apigateway.LambdaIntegration(listTasksFn));
    projectTasks.addMethod("POST", new apigateway.LambdaIntegration(createTaskFn));

    const tasks = this.api.root.addResource("tasks");
    const task = tasks.addResource("{taskId}");
    task.addMethod("GET", new apigateway.LambdaIntegration(getTaskFn));
    task.addMethod("PUT", new apigateway.LambdaIntegration(updateTaskFn));
    task.addMethod("DELETE", new apigateway.LambdaIntegration(deleteTaskFn));

    new cdk.CfnOutput(this, "ApiUrl", {
      value: this.api.url,
      description: "API Gateway URL",
    });
  }
}

Lambda Handler Stub

// src/handlers/projects/list.ts
import { APIGatewayProxyHandler } from "aws-lambda";

export const handler: APIGatewayProxyHandler = async (event) => {
  // Retrieve auth info from event.requestContext.authorizer
  const userId = event.requestContext.authorizer?.claims?.sub;

  try {
    // TODO: fetch project list from DynamoDB
    const projects: unknown[] = [];

    return {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
        // CORS headers required in proxy integration responses
        "Access-Control-Allow-Origin": "*",
      },
      body: JSON.stringify({ projects }),
    };
  } catch (err) {
    console.error("Error listing projects:", err);
    return {
      statusCode: 500,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: "Internal server error" }),
    };
  }
};

Step 3: Authentication (Cognito / Lambda Authorizer / API Key)

API Gateway supports three auth patterns. Describe your use case to Claude Code and it will recommend the right one.

Auth Selection Prompt

Recommend the best API Gateway auth method for each case, with CDK implementation:
- BtoC mobile app API (user authentication required)
- Third-party API exposure (API key management)
- Internal tool (reuse existing Active Directory auth)

Pattern 1: Cognito User Pool Authorizer (BtoC)

import * as cognito from "aws-cdk-lib/aws-cognito";

const userPool = new cognito.UserPool(this, "UserPool", {
  userPoolName: "task-app-users",
  selfSignUpEnabled: true,
  signInAliases: { email: true },
  passwordPolicy: {
    minLength: 8,
    requireLowercase: true,
    requireUppercase: true,
    requireDigits: true,
    requireSymbols: false,
  },
  accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
});

const userPoolClient = userPool.addClient("AppClient", {
  authFlows: {
    userPassword: true,
    userSrp: true,
  },
  accessTokenValidity: cdk.Duration.hours(1),
  refreshTokenValidity: cdk.Duration.days(30),
});

const cognitoAuthorizer = new apigateway.CognitoUserPoolsAuthorizer(
  this,
  "CognitoAuthorizer",
  {
    cognitoUserPools: [userPool],
    identitySource: "method.request.header.Authorization",
    resultsCacheTtl: cdk.Duration.minutes(5), // cache for cost savings
  }
);

// Apply to methods requiring authentication
projects.addMethod("GET", new apigateway.LambdaIntegration(listProjectsFn), {
  authorizer: cognitoAuthorizer,
  authorizationType: apigateway.AuthorizationType.COGNITO,
});

Pattern 2: Lambda Authorizer (Custom Auth)

// src/handlers/auth/authorizer.ts
import {
  APIGatewayAuthorizerResult,
  APIGatewayTokenAuthorizerHandler,
} from "aws-lambda";
import * as jwt from "jsonwebtoken";

export const handler: APIGatewayTokenAuthorizerHandler = async (event) => {
  const token = event.authorizationToken.replace("Bearer ", "");

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
      sub: string;
      email: string;
      role: string;
    };

    return generatePolicy(decoded.sub, "Allow", event.methodArn, {
      userId: decoded.sub,
      email: decoded.email,
      role: decoded.role,
    });
  } catch {
    // Throw "Unauthorized" to return a 401
    throw new Error("Unauthorized");
  }
};

function generatePolicy(
  principalId: string,
  effect: "Allow" | "Deny",
  resource: string,
  context?: Record<string, string>
): APIGatewayAuthorizerResult {
  return {
    principalId,
    policyDocument: {
      Version: "2012-10-17",
      Statement: [
        {
          Action: "execute-api:Invoke",
          Effect: effect,
          // Wildcard maximizes cache benefit
          Resource: resource.replace(/\/[^/]+\/[^/]+$/, "/*/*"),
        },
      ],
    },
    context, // accessible from event.requestContext.authorizer in handlers
  };
}

Pattern 3: API Key (Third-Party)

const apiKey = this.api.addApiKey("PartnerApiKey", {
  apiKeyName: "partner-v1-key",
  description: "API Key for partner integration",
});

const usagePlan = this.api.addUsagePlan("PartnerUsagePlan", {
  name: "PartnerPlan",
  throttle: {
    rateLimit: 100,   // requests per second
    burstLimit: 200,
  },
  quota: {
    limit: 10000,     // requests per day
    period: apigateway.Period.DAY,
  },
});

usagePlan.addApiKey(apiKey);
usagePlan.addApiStage({ stage: this.api.deploymentStage });

Step 4: CDK Infrastructure Implementation

Pull everything together into a production-ready CDK stack structure.

Project Layout

my-api/
├── bin/
│   └── my-api.ts
├── lib/
│   ├── api-stack.ts
│   ├── auth-stack.ts
│   └── database-stack.ts
├── src/
│   └── handlers/
│       ├── projects/
│       └── tasks/
├── package.json
├── tsconfig.json
└── cdk.json

bin/my-api.ts (Entry Point)

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { ApiStack } from "../lib/api-stack";
import { AuthStack } from "../lib/auth-stack";

const app = new cdk.App();
const env = process.env.CDK_ENV ?? "dev";

const authStack = new AuthStack(app, `Auth-${env}`, {
  env: { region: "ap-northeast-1" },
});

new ApiStack(app, `Api-${env}`, {
  env: { region: "ap-northeast-1" },
  userPool: authStack.userPool,
  stageName: env,
});

Granting Lambda IAM Permissions

import * as dynamodb from "aws-cdk-lib/aws-dynamodb";

const projectsTable = new dynamodb.Table(this, "ProjectsTable", {
  tableName: `projects-${stageName}`,
  partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
  sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy:
    stageName === "prod"
      ? cdk.RemovalPolicy.RETAIN
      : cdk.RemovalPolicy.DESTROY,
});

projectsTable.grantReadWriteData(listProjectsFn);
projectsTable.grantReadWriteData(createProjectFn);
projectsTable.grantReadData(getProjectFn); // read-only
projectsTable.grantReadWriteData(updateProjectFn);
projectsTable.grantReadWriteData(deleteProjectFn);

Step 5: Stage Management (dev/staging/prod)

Environment Config Separation

// lib/config.ts
export type Stage = "dev" | "staging" | "prod";

interface StageConfig {
  logLevel: "ERROR" | "INFO" | "DEBUG";
  throttleRateLimit: number;
  throttleBurstLimit: number;
  corsOrigins: string[];
  enableDataTrace: boolean;
}

export const stageConfig: Record<Stage, StageConfig> = {
  dev: {
    logLevel: "DEBUG",
    throttleRateLimit: 10,
    throttleBurstLimit: 20,
    corsOrigins: ["http://localhost:3000", "http://localhost:5173"],
    enableDataTrace: true,
  },
  staging: {
    logLevel: "INFO",
    throttleRateLimit: 50,
    throttleBurstLimit: 100,
    corsOrigins: ["https://staging.example.com"],
    enableDataTrace: false,
  },
  prod: {
    logLevel: "ERROR",
    throttleRateLimit: 1000,
    throttleBurstLimit: 2000,
    corsOrigins: ["https://example.com", "https://app.example.com"],
    enableDataTrace: false,
  },
};

Deploy Commands

# Deploy to dev
CDK_ENV=dev npx cdk deploy Api-dev --require-approval never

# Staging
CDK_ENV=staging npx cdk deploy Api-staging

# Production (with change confirmation)
CDK_ENV=prod npx cdk deploy Api-prod

# Always diff before deploying to production
CDK_ENV=prod npx cdk diff Api-prod

4 Common Pitfalls

These are the subtle but painful traps I’ve actually hit with API Gateway. Tell Claude Code upfront “avoid these pitfalls” and it will handle them proactively.

Pitfall 1: Missing CORS Configuration (Most Common)

Symptom: fetch() from the browser throws a CORS error. Postman works fine.

Root cause: Even with CORS enabled on API Gateway, Lambda (proxy integration) is responsible for returning CORS headers in the response. You must set headers in Lambda too.

// ❌ Insufficient — only API Gateway config
defaultCorsPreflightOptions: {
  allowOrigins: apigateway.Cors.ALL_ORIGINS,
}

// ✅ Always add CORS headers in Lambda responses
return {
  statusCode: 200,
  headers: {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "https://example.com",
    "Access-Control-Allow-Credentials": "true", // if using cookies
  },
  body: JSON.stringify(data),
};

Pitfall 2: Missing Lambda Invoke Permission

Symptom: API returns {"message": "Internal server error"}. CloudWatch logs show AccessDeniedException.

Root cause: API Gateway lacks permission to invoke the Lambda function. CDK’s LambdaIntegration grants this automatically, but it’s easy to miss when adding resources manually.

// Manually grant permission if needed
listProjectsFn.addPermission("ApiGatewayInvoke", {
  principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
  sourceArn: this.api.arnForExecuteApi("GET", "/projects", "v1"),
});

Pitfall 3: The 29-Second Timeout Limit

Symptom: Requests taking over 30 seconds suddenly fail with {"message": "Endpoint request timed out"}.

Root cause: API Gateway’s integration timeout is capped at 29 seconds. This cannot be raised.

Solution: Offload long-running work asynchronously.

// ✅ Async pattern: return a job ID immediately, then poll for status
export const startExport: APIGatewayProxyHandler = async (event) => {
  const jobId = crypto.randomUUID();

  // Queue the job in SQS
  await sqsClient.send(
    new SendMessageCommand({
      QueueUrl: process.env.JOB_QUEUE_URL!,
      MessageBody: JSON.stringify({ jobId, params: JSON.parse(event.body!) }),
    })
  );

  return {
    statusCode: 202, // Accepted
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jobId, status: "processing" }),
  };
};

// GET /export/{jobId}/status
export const getExportStatus: APIGatewayProxyHandler = async (event) => {
  const { jobId } = event.pathParameters!;
  // Fetch job status from DynamoDB
  // ...
};

Pitfall 4: Missing Request Body in Lambda (Mapping Templates)

Symptom: Lambda receives null for event.body.

Root cause: Non-proxy integrations require mapping templates. Modern setups should always use proxy integration, but you may encounter the mismatch in legacy system migrations.

// ✅ Always explicitly set proxy: true
project.addMethod(
  "POST",
  new apigateway.LambdaIntegration(createProjectFn, {
    proxy: true, // default is true, but being explicit avoids surprises
  })
);

Summary

TaskClaude Code ValueDifficulty
REST API DesignAuto-generate specs from use cases, REST principle checkLow
Lambda Integration CDKGenerate proxy integration + resource definitions in bulkLow
Cognito AuthGenerate User Pool, Authorizer, and client configMedium
Lambda AuthorizerImplement JWT validation + policy generationMedium
Stage ManagementSeparate env configs, generate deploy commandsMedium
Async ProcessingDesign SQS integration + polling patternHigh
CORS ConfigurationGenerate Lambda-side headers without omissionsLow

The biggest win I found was using Claude Code as a design reviewer before writing any code. Asking “are there any REST violations in this endpoint design?” catches URL consistency issues, idempotency problems, and HTTP method misuse — the kinds of things that normally surface in code review.

The reason implementation time dropped to one-third was fewer rework cycles. Better design quality upfront meant far fewer post-implementation fixes.


Applying these patterns in a professional project: API Gateway design-to-CDK-deploy time went from 3 days to 1 day. The Cognito Authorizer setup alone — which used to require over an hour of AWS doc reading — now resolves with a single Claude Code prompt.


References

#claude-code #aws #api-gateway #lambda #typescript #rest-api

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Just enter your email and we'll send you the single-page A4 cheatsheet right away.

We handle your data with care and never send spam.

Masa

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.