Tools: Powering Your App with Serverless APIs: API Gateway & Lambda Integration

Tools: Powering Your App with Serverless APIs: API Gateway & Lambda Integration

Source: Dev.to

Table of Contents ## Introduction ## Why API Gateway + Lambda? ## πŸ”Œ API Gateway Benefits ## ⚑ Lambda Benefits ## πŸ’° The Cost Advantage ## What We're Building ## Project Setup ## Creating Lambda Functions ## Health Check Handler ## Leads Handler ## Building the API Gateway ## Deep Dive: Proxy vs. Non-Proxy Integration ## CORS Configuration Deep Dive ## What is CORS? ## What Headers Do We Need? ## API Gateway CORS vs Lambda CORS ## Production Tip ## Deployment ## Testing Your API ## Using curl ## Using Postman ## Connecting to the Frontend ## Gotchas & Common Pitfalls ## 1. CORS Errors ## 2. 502 Bad Gateway ## 3. Lambda Proxy Integration Response Format ## 4. Path Parameters Not Working ## Best Practices ## 1. Use Lambda Proxy Integration ## 2. Implement Request Validation ## 3. Consistent Error Responses ## 4. Add Request Throttling ## 5. Enable CloudWatch Logging ## Cost Considerations ## Real-World Example ## Conclusion ## References In Part 1, we launched a beautiful contact form website using S3 and CloudFront. But there's a problem – our form doesn't actually do anything yet. Click "Send Message" and... nothing happens. The data just vanishes into the void. Today, we're going to change that. Imagine you're building a lead generation system for your business. Every contact matters. You need a reliable, scalable backend that can handle one request or a million – without breaking a sweat or your budget. That's exactly what API Gateway and Lambda deliver. In this second part of our Serverless Web Mastery series, we'll transform our static website into a dynamic application by creating a fully-functional REST API. By the end of this post, you'll have an API that can create, read, update, and delete leads – all without managing a single server. Let's make that form come alive! Before diving into code, let's understand why this combination is so powerful: Traditional server: ~$40-100/month (even when idle) Serverless (1000 requests/day): ~$0.50/month That's a 99% cost reduction for most small to medium workloads! Our Contact Form API will have these endpoints: Architecture Overview: Let's create the Part 2 directory structure: Install dependencies: A simple health check is essential for monitoring. Create lambda/handlers/health.ts: Why a health endpoint? Now for the main event – our leads CRUD handler. Create lambda/handlers/leads.ts: Important Note: This uses in-memory storage for simplicity. In Part 3, we'll replace this with DynamoDB for persistent storage. Now let's wire everything together with CDK. Create lib/api-lambda-stack.ts: You'll notice we used { proxy: true } in our Lambda integration. What does this mean? Lambda Proxy Integration (Recommended) Non-Proxy Integration For modern serverless applications, Proxy Integration is the standard because it keeps the infrastructure simple (no VTL) and moves the logic into the code (TypeScript), which is where we want it! CORS (Cross-Origin Resource Sharing) is often the #1 source of frustration when building APIs. Let me save you hours of debugging: When your frontend (hosted on d123.cloudfront.net) makes a request to your API (abc.execute-api.amazonaws.com), browsers block this by default. CORS headers tell the browser "it's okay, allow this request." You need CORS in both places: Replace * with your specific domain: You'll see outputs like: Import this collection for easy testing: Update your Part 1 frontend app.js: Now your form actually works! πŸŽ‰ Problem: "Access to fetch has been blocked by CORS policy" Solution: Check both: Problem: API returns 502 error Solution: Check CloudWatch logs: Problem: API Gateway doesn't understand Lambda response Solution: Always return this exact format: Problem: pathParameters is null or undefined Solution: Ensure the path parameter is defined in API Gateway: Always use proxy integration for flexibility: Validate early, fail fast: Use a standard error format: Protect your API from abuse: Debugging is impossible without logs: Let's break down the costs: For a contact form receiving 1,000 submissions/day: Even at 100K submissions/day, you're looking at ~$5/month. Congratulations! πŸŽ‰ You've built a production-ready REST API with: βœ… Multiple Lambda functions βœ… RESTful API Gateway endpoints βœ… CORS configuration for browser access βœ… Input validation βœ… Error handling βœ… CloudWatch logging βœ… Request throttling But wait – there's a catch. Our current implementation uses in-memory storage, which means data is lost when Lambda recycles. In Part 3, we'll fix this by adding DynamoDB for persistent storage. You'll learn: If you're stuck on any CDK issues, my AWS CDK Debugging Guide has got you covered. GitHub Repository: aws-serverless-website-tutorial See you until next time. Happy coding! πŸš€ Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Endpoints β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ GET /health β†’ Health check (monitoring) β”‚ β”‚ POST /leads β†’ Create a new lead β”‚ β”‚ GET /leads β†’ List all leads (admin) β”‚ β”‚ GET /leads/{id} β†’ Get specific lead β”‚ β”‚ PUT /leads/{id} β†’ Update a lead β”‚ β”‚ DELETE /leads/{id} β†’ Delete a lead β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Endpoints β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ GET /health β†’ Health check (monitoring) β”‚ β”‚ POST /leads β†’ Create a new lead β”‚ β”‚ GET /leads β†’ List all leads (admin) β”‚ β”‚ GET /leads/{id} β†’ Get specific lead β”‚ β”‚ PUT /leads/{id} β†’ Update a lead β”‚ β”‚ DELETE /leads/{id} β†’ Delete a lead β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Endpoints β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ GET /health β†’ Health check (monitoring) β”‚ β”‚ POST /leads β†’ Create a new lead β”‚ β”‚ GET /leads β†’ List all leads (admin) β”‚ β”‚ GET /leads/{id} β†’ Get specific lead β”‚ β”‚ PUT /leads/{id} β†’ Update a lead β”‚ β”‚ DELETE /leads/{id} β†’ Delete a lead β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CloudFront β”‚ β”‚ (Part 1 Site) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Gateway β”‚ β”‚ (REST API) β”‚ β”‚ β”‚ β”‚ β€’ CORS configuration β”‚ β”‚ β€’ Request throttling β”‚ β”‚ β€’ CloudWatch logging β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Health β”‚ β”‚ Leads β”‚ β”‚ Future β”‚ β”‚ Handler β”‚ β”‚ Handler β”‚ β”‚ Handlers β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CloudFront β”‚ β”‚ (Part 1 Site) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Gateway β”‚ β”‚ (REST API) β”‚ β”‚ β”‚ β”‚ β€’ CORS configuration β”‚ β”‚ β€’ Request throttling β”‚ β”‚ β€’ CloudWatch logging β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Health β”‚ β”‚ Leads β”‚ β”‚ Future β”‚ β”‚ Handler β”‚ β”‚ Handler β”‚ β”‚ Handlers β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CloudFront β”‚ β”‚ (Part 1 Site) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API Gateway β”‚ β”‚ (REST API) β”‚ β”‚ β”‚ β”‚ β€’ CORS configuration β”‚ β”‚ β€’ Request throttling β”‚ β”‚ β€’ CloudWatch logging β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Health β”‚ β”‚ Leads β”‚ β”‚ Future β”‚ β”‚ Handler β”‚ β”‚ Handler β”‚ β”‚ Handlers β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β”‚ (Lambda) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ CODE_BLOCK: cd aws-serverless-website-tutorial mkdir -p part-2-api-lambda/{cdk,lambda/handlers} cd part-2-api-lambda/cdk Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: cd aws-serverless-website-tutorial mkdir -p part-2-api-lambda/{cdk,lambda/handlers} cd part-2-api-lambda/cdk CODE_BLOCK: cd aws-serverless-website-tutorial mkdir -p part-2-api-lambda/{cdk,lambda/handlers} cd part-2-api-lambda/cdk CODE_BLOCK: { "name": "api-lambda-cdk", "version": "1.0.0", "scripts": { "build": "tsc", "deploy": "cdk deploy", "destroy": "cdk destroy" }, "dependencies": { "aws-cdk-lib": "^2.170.0", "constructs": "^10.4.2" }, "devDependencies": { "@types/node": "^22.10.0", "typescript": "~5.7.0", "aws-cdk": "^2.170.0" } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "name": "api-lambda-cdk", "version": "1.0.0", "scripts": { "build": "tsc", "deploy": "cdk deploy", "destroy": "cdk destroy" }, "dependencies": { "aws-cdk-lib": "^2.170.0", "constructs": "^10.4.2" }, "devDependencies": { "@types/node": "^22.10.0", "typescript": "~5.7.0", "aws-cdk": "^2.170.0" } } CODE_BLOCK: { "name": "api-lambda-cdk", "version": "1.0.0", "scripts": { "build": "tsc", "deploy": "cdk deploy", "destroy": "cdk destroy" }, "dependencies": { "aws-cdk-lib": "^2.170.0", "constructs": "^10.4.2" }, "devDependencies": { "@types/node": "^22.10.0", "typescript": "~5.7.0", "aws-cdk": "^2.170.0" } } COMMAND_BLOCK: npm install Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install COMMAND_BLOCK: npm install COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; interface HealthResponse { status: "healthy" | "degraded" | "unhealthy"; timestamp: string; version: string; region: string; } export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { console.log("Health check requested", { requestId: context.awsRequestId, }); const response: HealthResponse = { status: "healthy", timestamp: new Date().toISOString(), version: "1.0.0", region: process.env.AWS_REGION || "unknown", }; return { statusCode: 200, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, body: JSON.stringify(response), }; }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; interface HealthResponse { status: "healthy" | "degraded" | "unhealthy"; timestamp: string; version: string; region: string; } export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { console.log("Health check requested", { requestId: context.awsRequestId, }); const response: HealthResponse = { status: "healthy", timestamp: new Date().toISOString(), version: "1.0.0", region: process.env.AWS_REGION || "unknown", }; return { statusCode: 200, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, body: JSON.stringify(response), }; }; COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; interface HealthResponse { status: "healthy" | "degraded" | "unhealthy"; timestamp: string; version: string; region: string; } export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { console.log("Health check requested", { requestId: context.awsRequestId, }); const response: HealthResponse = { status: "healthy", timestamp: new Date().toISOString(), version: "1.0.0", region: process.env.AWS_REGION || "unknown", }; return { statusCode: 200, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, body: JSON.stringify(response), }; }; COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; // Lead interface interface Lead { id: string; name: string; email: string; company?: string; subject: string; message: string; status: "new" | "contacted" | "qualified" | "converted"; createdAt: string; updatedAt: string; } // In-memory storage (replaced with DynamoDB in Part 3) const leadsStorage: Map<string, Lead> = new Map(); // Generate unique ID function generateId(): string { return `lead_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } // Validate create request function validateCreateRequest(body: any): { valid: boolean; error?: string } { if (!body.name || body.name.length < 2) { return { valid: false, error: "Name is required (min 2 characters)" }; } if (!body.email || !body.email.includes("@")) { return { valid: false, error: "Valid email is required" }; } if (!body.message || body.message.length < 10) { return { valid: false, error: "Message is required (min 10 characters)" }; } return { valid: true }; } // Create standardized response function createResponse( statusCode: number, body: object, ): APIGatewayProxyResult { return { statusCode, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", }, body: JSON.stringify(body), }; } // Main handler export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { const { httpMethod, pathParameters, body } = event; const leadId = pathParameters?.id; console.log("Request:", { method: httpMethod, leadId, requestId: context.awsRequestId, }); try { switch (httpMethod) { // Create new lead case "POST": { const parsedBody = JSON.parse(body || "{}"); const validation = validateCreateRequest(parsedBody); if (!validation.valid) { return createResponse(400, { success: false, error: validation.error, }); } const now = new Date().toISOString(); const lead: Lead = { id: generateId(), name: parsedBody.name.trim(), email: parsedBody.email.toLowerCase().trim(), company: parsedBody.company?.trim(), subject: parsedBody.subject || "general", message: parsedBody.message.trim(), status: "new", createdAt: now, updatedAt: now, }; leadsStorage.set(lead.id, lead); console.log("Lead created:", lead.id); return createResponse(201, { success: true, data: lead, message: "Lead created successfully", }); } // List all leads case "GET": { if (leadId) { const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, data: lead }); } const leads = Array.from(leadsStorage.values()); return createResponse(200, { success: true, data: leads, message: `Found ${leads.length} lead(s)`, }); } // Update lead case "PUT": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } const updates = JSON.parse(body || "{}"); const updatedLead: Lead = { ...lead, ...updates, updatedAt: new Date().toISOString(), }; leadsStorage.set(leadId, updatedLead); return createResponse(200, { success: true, data: updatedLead }); } // Delete lead case "DELETE": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const deleted = leadsStorage.delete(leadId); if (!deleted) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, message: "Lead deleted" }); } default: return createResponse(405, { success: false, error: "Method not allowed", }); } } catch (error) { console.error("Error:", error); return createResponse(500, { success: false, error: "Internal server error", }); } }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; // Lead interface interface Lead { id: string; name: string; email: string; company?: string; subject: string; message: string; status: "new" | "contacted" | "qualified" | "converted"; createdAt: string; updatedAt: string; } // In-memory storage (replaced with DynamoDB in Part 3) const leadsStorage: Map<string, Lead> = new Map(); // Generate unique ID function generateId(): string { return `lead_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } // Validate create request function validateCreateRequest(body: any): { valid: boolean; error?: string } { if (!body.name || body.name.length < 2) { return { valid: false, error: "Name is required (min 2 characters)" }; } if (!body.email || !body.email.includes("@")) { return { valid: false, error: "Valid email is required" }; } if (!body.message || body.message.length < 10) { return { valid: false, error: "Message is required (min 10 characters)" }; } return { valid: true }; } // Create standardized response function createResponse( statusCode: number, body: object, ): APIGatewayProxyResult { return { statusCode, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", }, body: JSON.stringify(body), }; } // Main handler export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { const { httpMethod, pathParameters, body } = event; const leadId = pathParameters?.id; console.log("Request:", { method: httpMethod, leadId, requestId: context.awsRequestId, }); try { switch (httpMethod) { // Create new lead case "POST": { const parsedBody = JSON.parse(body || "{}"); const validation = validateCreateRequest(parsedBody); if (!validation.valid) { return createResponse(400, { success: false, error: validation.error, }); } const now = new Date().toISOString(); const lead: Lead = { id: generateId(), name: parsedBody.name.trim(), email: parsedBody.email.toLowerCase().trim(), company: parsedBody.company?.trim(), subject: parsedBody.subject || "general", message: parsedBody.message.trim(), status: "new", createdAt: now, updatedAt: now, }; leadsStorage.set(lead.id, lead); console.log("Lead created:", lead.id); return createResponse(201, { success: true, data: lead, message: "Lead created successfully", }); } // List all leads case "GET": { if (leadId) { const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, data: lead }); } const leads = Array.from(leadsStorage.values()); return createResponse(200, { success: true, data: leads, message: `Found ${leads.length} lead(s)`, }); } // Update lead case "PUT": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } const updates = JSON.parse(body || "{}"); const updatedLead: Lead = { ...lead, ...updates, updatedAt: new Date().toISOString(), }; leadsStorage.set(leadId, updatedLead); return createResponse(200, { success: true, data: updatedLead }); } // Delete lead case "DELETE": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const deleted = leadsStorage.delete(leadId); if (!deleted) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, message: "Lead deleted" }); } default: return createResponse(405, { success: false, error: "Method not allowed", }); } } catch (error) { console.error("Error:", error); return createResponse(500, { success: false, error: "Internal server error", }); } }; COMMAND_BLOCK: import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, } from "aws-lambda"; // Lead interface interface Lead { id: string; name: string; email: string; company?: string; subject: string; message: string; status: "new" | "contacted" | "qualified" | "converted"; createdAt: string; updatedAt: string; } // In-memory storage (replaced with DynamoDB in Part 3) const leadsStorage: Map<string, Lead> = new Map(); // Generate unique ID function generateId(): string { return `lead_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } // Validate create request function validateCreateRequest(body: any): { valid: boolean; error?: string } { if (!body.name || body.name.length < 2) { return { valid: false, error: "Name is required (min 2 characters)" }; } if (!body.email || !body.email.includes("@")) { return { valid: false, error: "Valid email is required" }; } if (!body.message || body.message.length < 10) { return { valid: false, error: "Message is required (min 10 characters)" }; } return { valid: true }; } // Create standardized response function createResponse( statusCode: number, body: object, ): APIGatewayProxyResult { return { statusCode, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", }, body: JSON.stringify(body), }; } // Main handler export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { const { httpMethod, pathParameters, body } = event; const leadId = pathParameters?.id; console.log("Request:", { method: httpMethod, leadId, requestId: context.awsRequestId, }); try { switch (httpMethod) { // Create new lead case "POST": { const parsedBody = JSON.parse(body || "{}"); const validation = validateCreateRequest(parsedBody); if (!validation.valid) { return createResponse(400, { success: false, error: validation.error, }); } const now = new Date().toISOString(); const lead: Lead = { id: generateId(), name: parsedBody.name.trim(), email: parsedBody.email.toLowerCase().trim(), company: parsedBody.company?.trim(), subject: parsedBody.subject || "general", message: parsedBody.message.trim(), status: "new", createdAt: now, updatedAt: now, }; leadsStorage.set(lead.id, lead); console.log("Lead created:", lead.id); return createResponse(201, { success: true, data: lead, message: "Lead created successfully", }); } // List all leads case "GET": { if (leadId) { const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, data: lead }); } const leads = Array.from(leadsStorage.values()); return createResponse(200, { success: true, data: leads, message: `Found ${leads.length} lead(s)`, }); } // Update lead case "PUT": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const lead = leadsStorage.get(leadId); if (!lead) { return createResponse(404, { success: false, error: "Lead not found", }); } const updates = JSON.parse(body || "{}"); const updatedLead: Lead = { ...lead, ...updates, updatedAt: new Date().toISOString(), }; leadsStorage.set(leadId, updatedLead); return createResponse(200, { success: true, data: updatedLead }); } // Delete lead case "DELETE": { if (!leadId) { return createResponse(400, { success: false, error: "Lead ID required", }); } const deleted = leadsStorage.delete(leadId); if (!deleted) { return createResponse(404, { success: false, error: "Lead not found", }); } return createResponse(200, { success: true, message: "Lead deleted" }); } default: return createResponse(405, { success: false, error: "Method not allowed", }); } } catch (error) { console.error("Error:", error); return createResponse(500, { success: false, error: "Internal server error", }); } }; CODE_BLOCK: import * as cdk from "aws-cdk-lib"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as logs from "aws-cdk-lib/aws-logs"; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs"; import { Construct } from "constructs"; import * as path from "path"; export class ApiLambdaStack extends cdk.Stack { public readonly api: apigateway.RestApi; constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ============================================ // Lambda Functions // ============================================ // Health Check Handler const healthHandler = new lambdaNodejs.NodejsFunction( this, "HealthHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/health.ts"), handler: "handler", description: "Health check endpoint", timeout: cdk.Duration.seconds(10), memorySize: 128, logRetention: logs.RetentionDays.ONE_WEEK, // Environment variables environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }, ); // Leads Handler const leadsHandler = new lambdaNodejs.NodejsFunction(this, "LeadsHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/leads.ts"), handler: "handler", description: "Leads CRUD operations", timeout: cdk.Duration.seconds(30), memorySize: 256, logRetention: logs.RetentionDays.ONE_WEEK, environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }); // ============================================ // API Gateway // ============================================ this.api = new apigateway.RestApi(this, "ContactFormApi", { restApiName: "Contact Form API", description: "Serverless API for contact form", deployOptions: { stageName: "prod", loggingLevel: apigateway.MethodLoggingLevel.INFO, metricsEnabled: true, throttlingBurstLimit: 100, throttlingRateLimit: 50, }, // CORS - Critical for browser requests! defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, allowHeaders: ["Content-Type", "Authorization"], allowCredentials: true, }, }); // ============================================ // API Routes // ============================================ // Health endpoint: GET /health const apiResource = this.api.root.addResource('api'); const healthResource = apiResource.addResource('health'); healthResource.addMethod('GET', new apigateway.LambdaIntegration(healthHandler, { proxy: true, // Use Lambda Proxy Integration })); // Leads endpoints const leadsResource = apiResource.addResource('leads'); // POST /leads - Create a new lead leadsResource.addMethod('POST', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // GET /leads - List all leads leadsResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // Single lead endpoints: /leads/{id} const leadByIdResource = leadsResource.addResource('{id}'); // GET /leads/{id} - Get specific lead leadByIdResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // PUT /leads/{id} - Update a lead leadByIdResource.addMethod('PUT', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // DELETE /leads/{id} - Delete a lead leadByIdResource.addMethod('DELETE', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // ============================================ // Outputs // ============================================ new cdk.CfnOutput(this, "ApiEndpoint", { value: this.api.url, description: "API Gateway endpoint URL", }); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import * as cdk from "aws-cdk-lib"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as logs from "aws-cdk-lib/aws-logs"; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs"; import { Construct } from "constructs"; import * as path from "path"; export class ApiLambdaStack extends cdk.Stack { public readonly api: apigateway.RestApi; constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ============================================ // Lambda Functions // ============================================ // Health Check Handler const healthHandler = new lambdaNodejs.NodejsFunction( this, "HealthHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/health.ts"), handler: "handler", description: "Health check endpoint", timeout: cdk.Duration.seconds(10), memorySize: 128, logRetention: logs.RetentionDays.ONE_WEEK, // Environment variables environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }, ); // Leads Handler const leadsHandler = new lambdaNodejs.NodejsFunction(this, "LeadsHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/leads.ts"), handler: "handler", description: "Leads CRUD operations", timeout: cdk.Duration.seconds(30), memorySize: 256, logRetention: logs.RetentionDays.ONE_WEEK, environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }); // ============================================ // API Gateway // ============================================ this.api = new apigateway.RestApi(this, "ContactFormApi", { restApiName: "Contact Form API", description: "Serverless API for contact form", deployOptions: { stageName: "prod", loggingLevel: apigateway.MethodLoggingLevel.INFO, metricsEnabled: true, throttlingBurstLimit: 100, throttlingRateLimit: 50, }, // CORS - Critical for browser requests! defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, allowHeaders: ["Content-Type", "Authorization"], allowCredentials: true, }, }); // ============================================ // API Routes // ============================================ // Health endpoint: GET /health const apiResource = this.api.root.addResource('api'); const healthResource = apiResource.addResource('health'); healthResource.addMethod('GET', new apigateway.LambdaIntegration(healthHandler, { proxy: true, // Use Lambda Proxy Integration })); // Leads endpoints const leadsResource = apiResource.addResource('leads'); // POST /leads - Create a new lead leadsResource.addMethod('POST', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // GET /leads - List all leads leadsResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // Single lead endpoints: /leads/{id} const leadByIdResource = leadsResource.addResource('{id}'); // GET /leads/{id} - Get specific lead leadByIdResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // PUT /leads/{id} - Update a lead leadByIdResource.addMethod('PUT', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // DELETE /leads/{id} - Delete a lead leadByIdResource.addMethod('DELETE', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // ============================================ // Outputs // ============================================ new cdk.CfnOutput(this, "ApiEndpoint", { value: this.api.url, description: "API Gateway endpoint URL", }); } } CODE_BLOCK: import * as cdk from "aws-cdk-lib"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as logs from "aws-cdk-lib/aws-logs"; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs"; import { Construct } from "constructs"; import * as path from "path"; export class ApiLambdaStack extends cdk.Stack { public readonly api: apigateway.RestApi; constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ============================================ // Lambda Functions // ============================================ // Health Check Handler const healthHandler = new lambdaNodejs.NodejsFunction( this, "HealthHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/health.ts"), handler: "handler", description: "Health check endpoint", timeout: cdk.Duration.seconds(10), memorySize: 128, logRetention: logs.RetentionDays.ONE_WEEK, // Environment variables environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }, ); // Leads Handler const leadsHandler = new lambdaNodejs.NodejsFunction(this, "LeadsHandler", { runtime: lambda.Runtime.NODEJS_22_X, entry: path.join(__dirname, "../../lambda/handlers/leads.ts"), handler: "handler", description: "Leads CRUD operations", timeout: cdk.Duration.seconds(30), memorySize: 256, logRetention: logs.RetentionDays.ONE_WEEK, environment: { NODE_ENV: "production", LOG_LEVEL: "INFO", }, bundling: { minify: true, sourceMap: true, }, }); // ============================================ // API Gateway // ============================================ this.api = new apigateway.RestApi(this, "ContactFormApi", { restApiName: "Contact Form API", description: "Serverless API for contact form", deployOptions: { stageName: "prod", loggingLevel: apigateway.MethodLoggingLevel.INFO, metricsEnabled: true, throttlingBurstLimit: 100, throttlingRateLimit: 50, }, // CORS - Critical for browser requests! defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, allowHeaders: ["Content-Type", "Authorization"], allowCredentials: true, }, }); // ============================================ // API Routes // ============================================ // Health endpoint: GET /health const apiResource = this.api.root.addResource('api'); const healthResource = apiResource.addResource('health'); healthResource.addMethod('GET', new apigateway.LambdaIntegration(healthHandler, { proxy: true, // Use Lambda Proxy Integration })); // Leads endpoints const leadsResource = apiResource.addResource('leads'); // POST /leads - Create a new lead leadsResource.addMethod('POST', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // GET /leads - List all leads leadsResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // Single lead endpoints: /leads/{id} const leadByIdResource = leadsResource.addResource('{id}'); // GET /leads/{id} - Get specific lead leadByIdResource.addMethod('GET', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // PUT /leads/{id} - Update a lead leadByIdResource.addMethod('PUT', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // DELETE /leads/{id} - Delete a lead leadByIdResource.addMethod('DELETE', new apigateway.LambdaIntegration(this.leadsHandler, { proxy: true, })); // ============================================ // Outputs // ============================================ new cdk.CfnOutput(this, "ApiEndpoint", { value: this.api.url, description: "API Gateway endpoint URL", }); } } CODE_BLOCK: 'Access-Control-Allow-Origin': '*' // or specific domain 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS' 'Access-Control-Allow-Headers': 'Content-Type,Authorization' 'Access-Control-Allow-Credentials': 'true' // if using cookies Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 'Access-Control-Allow-Origin': '*' // or specific domain 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS' 'Access-Control-Allow-Headers': 'Content-Type,Authorization' 'Access-Control-Allow-Credentials': 'true' // if using cookies CODE_BLOCK: 'Access-Control-Allow-Origin': '*' // or specific domain 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS' 'Access-Control-Allow-Headers': 'Content-Type,Authorization' 'Access-Control-Allow-Credentials': 'true' // if using cookies CODE_BLOCK: // In CDK (API Gateway level) defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, } // In Lambda (Response level) return { headers: { 'Access-Control-Allow-Origin': '*', }, // ... rest of response }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // In CDK (API Gateway level) defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, } // In Lambda (Response level) return { headers: { 'Access-Control-Allow-Origin': '*', }, // ... rest of response }; CODE_BLOCK: // In CDK (API Gateway level) defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, } // In Lambda (Response level) return { headers: { 'Access-Control-Allow-Origin': '*', }, // ... rest of response }; CODE_BLOCK: allowOrigins: ['https://yourdomain.com'], Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: allowOrigins: ['https://yourdomain.com'], CODE_BLOCK: allowOrigins: ['https://yourdomain.com'], CODE_BLOCK: cd part-2-api-lambda/cdk npm install npx cdk deploy Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: cd part-2-api-lambda/cdk npm install npx cdk deploy CODE_BLOCK: cd part-2-api-lambda/cdk npm install npx cdk deploy CODE_BLOCK: βœ… ContactFormApiStack Outputs: ContactFormApiStack.ApiEndpoint = https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: βœ… ContactFormApiStack Outputs: ContactFormApiStack.ApiEndpoint = https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/ CODE_BLOCK: βœ… ContactFormApiStack Outputs: ContactFormApiStack.ApiEndpoint = https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/ COMMAND_BLOCK: # Health check curl https://YOUR_API/health # Create a lead curl -X POST https://YOUR_API/leads \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "email": "[email protected]", "subject": "general", "message": "Testing the API!" }' # List leads curl https://YOUR_API/leads Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Health check curl https://YOUR_API/health # Create a lead curl -X POST https://YOUR_API/leads \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "email": "[email protected]", "subject": "general", "message": "Testing the API!" }' # List leads curl https://YOUR_API/leads COMMAND_BLOCK: # Health check curl https://YOUR_API/health # Create a lead curl -X POST https://YOUR_API/leads \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "email": "[email protected]", "subject": "general", "message": "Testing the API!" }' # List leads curl https://YOUR_API/leads CODE_BLOCK: const CONFIG = { // Replace with your actual API endpoint API_ENDPOINT: "https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod", }; async function submitToAPI(formData) { const response = await fetch(`${CONFIG.API_ENDPOINT}/leads`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to submit form"); } return response.json(); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const CONFIG = { // Replace with your actual API endpoint API_ENDPOINT: "https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod", }; async function submitToAPI(formData) { const response = await fetch(`${CONFIG.API_ENDPOINT}/leads`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to submit form"); } return response.json(); } CODE_BLOCK: const CONFIG = { // Replace with your actual API endpoint API_ENDPOINT: "https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod", }; async function submitToAPI(formData) { const response = await fetch(`${CONFIG.API_ENDPOINT}/leads`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to submit form"); } return response.json(); } CODE_BLOCK: // Make sure both have matching headers! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Make sure both have matching headers! CODE_BLOCK: // Make sure both have matching headers! CODE_BLOCK: aws logs tail /aws/lambda/YourFunctionName --follow Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: aws logs tail /aws/lambda/YourFunctionName --follow CODE_BLOCK: aws logs tail /aws/lambda/YourFunctionName --follow CODE_BLOCK: return { statusCode: 200, // Number, not string! headers: { ... }, body: JSON.stringify({ ... }), // Must be string! }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: return { statusCode: 200, // Number, not string! headers: { ... }, body: JSON.stringify({ ... }), // Must be string! }; CODE_BLOCK: return { statusCode: 200, // Number, not string! headers: { ... }, body: JSON.stringify({ ... }), // Must be string! }; CODE_BLOCK: const leadById = leads.addResource("{id}"); // Note the curly braces! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const leadById = leads.addResource("{id}"); // Note the curly braces! CODE_BLOCK: const leadById = leads.addResource("{id}"); // Note the curly braces! CODE_BLOCK: new apigateway.LambdaIntegration(handler, { proxy: true, // This is the default, but be explicit }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: new apigateway.LambdaIntegration(handler, { proxy: true, // This is the default, but be explicit }); CODE_BLOCK: new apigateway.LambdaIntegration(handler, { proxy: true, // This is the default, but be explicit }); CODE_BLOCK: function validateRequest(body: any): { valid: boolean; error?: string } { if (!body.email) return { valid: false, error: "Email required" }; if (!isValidEmail(body.email)) return { valid: false, error: "Invalid email" }; // ... more validations return { valid: true }; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function validateRequest(body: any): { valid: boolean; error?: string } { if (!body.email) return { valid: false, error: "Email required" }; if (!isValidEmail(body.email)) return { valid: false, error: "Invalid email" }; // ... more validations return { valid: true }; } CODE_BLOCK: function validateRequest(body: any): { valid: boolean; error?: string } { if (!body.email) return { valid: false, error: "Email required" }; if (!isValidEmail(body.email)) return { valid: false, error: "Invalid email" }; // ... more validations return { valid: true }; } CODE_BLOCK: interface ErrorResponse { success: false; error: string; code?: string; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: interface ErrorResponse { success: false; error: string; code?: string; } CODE_BLOCK: interface ErrorResponse { success: false; error: string; code?: string; } CODE_BLOCK: deployOptions: { throttlingBurstLimit: 100, // Max concurrent requests throttlingRateLimit: 50, // Requests per second } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: deployOptions: { throttlingBurstLimit: 100, // Max concurrent requests throttlingRateLimit: 50, // Requests per second } CODE_BLOCK: deployOptions: { throttlingBurstLimit: 100, // Max concurrent requests throttlingRateLimit: 50, // Requests per second } CODE_BLOCK: logRetention: logs.RetentionDays.ONE_WEEK, Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: logRetention: logs.RetentionDays.ONE_WEEK, CODE_BLOCK: logRetention: logs.RetentionDays.ONE_WEEK, - Introduction - Why API Gateway + Lambda? - What We're Building - Project Setup - Creating Lambda Functions - Building the API Gateway - CORS Configuration Deep Dive - Testing Your API - Connecting to the Frontend - Gotchas & Common Pitfalls - Best Practices - Cost Considerations - Load balancers need it - Monitoring systems poll it - Quick verification during deployment - How it works: API Gateway passes the entire HTTP request (headers, body, query params) to your Lambda function as a JSON object. - Your Responsibility: Your Lambda must return a response in a specific format: { statusCode: 200, body: "..." }. - Pros: Full control over the response, access to all request details, easier to test locally. - How it works: API Gateway transforms the incoming request using "Mapping Templates" before sending it to Lambda, and transforms the Lambda's response before sending it back to the client. - Pros: Decouples your backend logic from HTTP details. - Cons: Velocity Template Language (VTL) is hard to debug and maintain. - API Gateway - Handles OPTIONS preflight requests - Lambda Response - Includes headers in actual responses - Create new request - Set URL to your API endpoint - Set method (GET, POST, etc.) - For POST/PUT, add JSON body - API Gateway CORS configuration - Lambda response headers - Lambda threw an unhandled exception - Lambda timed out - Response format incorrect - API Gateway: 30K requests/month β†’ Free - Lambda: 30K invocations, ~100ms each β†’ Free - Total: $0/month (within free tier!) - DynamoDB table design - CRUD operations with AWS SDK v3 - Batch operations for performance (check out my DynamoDB Batch Operations blog) - Proper error handling and retries - API Gateway Developer Guide - Lambda Developer Guide - API Gateway CORS Documentation - Lambda Proxy Integration - API Gateway Pricing - Lambda Pricing