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
| Task | Claude Code Value | Difficulty |
|---|---|---|
| REST API Design | Auto-generate specs from use cases, REST principle check | Low |
| Lambda Integration CDK | Generate proxy integration + resource definitions in bulk | Low |
| Cognito Auth | Generate User Pool, Authorizer, and client config | Medium |
| Lambda Authorizer | Implement JWT validation + policy generation | Medium |
| Stage Management | Separate env configs, generate deploy commands | Medium |
| Async Processing | Design SQS integration + polling pattern | High |
| CORS Configuration | Generate Lambda-side headers without omissions | Low |
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.
Related Articles
- Claude Code × AWS Lambda Complete Guide
- Claude Code × REST API Design in Practice
- Claude Code × AWS IAM Permission Design Guide
References
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
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.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
Claude Code × Amazon Bedrock Complete Guide | Running Claude in Production on AWS
Complete guide to using Amazon Bedrock with Claude Code. From IAM authentication, streaming, Lambda integration, RAG implementation, to cost optimization — based on Masa's real production experience.
Claude Code × AWS CodePipeline/CodeBuild Complete Guide | Automate CI/CD Pipeline Build
Automatically build CI/CD with AWS CodePipeline & CodeBuild using Claude Code. Real code examples for pipeline design, buildspec.yml generation, test automation, and CDK infrastructure.
Claude Code × AWS CloudWatch Complete Guide | Log Analysis, Alarm Setup & Dashboard Automation
Boost AWS CloudWatch efficiency with Claude Code. Real-world code for log pattern analysis, automatic alarm configuration, metrics dashboards, and incident investigation.