API Gateway Lambda: Serverless API
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Why Modern Serverless API Architecture Differs
The serverless API landscape transformed significantly between 2023 and 2025. Lambda now supports response streaming for payloads exceeding 6MB, eliminating the previous hard limit that forced teams toward hybrid architectures. SnapStart reduces Java and .NET cold starts from 3-5 seconds to under 200ms, making these languages viable for latency-sensitive APIs. API Gateway HTTP APIs achieved feature parity with REST APIs while offering 70% cost reduction and native JWT validation without custom authorizers.
Modern compliance requirements fundamentally changed architectural decisions. GDPR, CCPA, and emerging AI regulations mandate request-level audit trails, data residency controls, and the ability to delete specific user data within 30 days. Serverless architectures naturally support these requirements through stateless execution, regional isolation, and event-driven audit logging. Traditional server-based APIs require extensive retrofitting to achieve equivalent compliance posture.
The rise of AI-powered applications introduced new constraints. LLM-based features require streaming responses to provide progressive user feedback. Vector database queries for semantic search need sub-second latency with unpredictable traffic patterns. Fine-tuning pipelines generate burst traffic that would overwhelm fixed-capacity infrastructure. The API Gateway Lambda pattern handles these requirements elegantly when implemented correctly.
Production-Grade Serverless API Architecture
A robust serverless API architecture in 2025 consists of multiple layers working in concert. API Gateway serves as the entry point, handling authentication, request validation, rate limiting, and response transformation. Lambda functions execute business logic in isolated, stateless environments. Supporting services like DynamoDB, EventBridge, and SQS provide data persistence, event routing, and asynchronous processing.
Here's a production-grade TypeScript implementation using AWS CDK that demonstrates modern patterns:
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 logs from 'aws-cdk-lib/aws-logs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class ServerlessApiStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDB table with on-demand billing and point-in-time recovery
const dataTable = new dynamodb.Table(this, 'ApiDataTable', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// Lambda function with optimized configuration
const apiHandler = new NodejsFunction(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'handler',
entry: 'src/handlers/api.ts',
timeout: cdk.Duration.seconds(29), // Just under API Gateway timeout
memorySize: 1769, // Price-performance sweet spot
architecture: lambda.Architecture.ARM_64,
environment: {
TABLE_NAME: dataTable.tableName,
NODE_OPTIONS: '--enable-source-maps',
POWERTOOLS_SERVICE_NAME: 'api-service',
POWERTOOLS_METRICS_NAMESPACE: 'ServerlessAPI',
LOG_LEVEL: 'INFO',
},
bundling: {
minify: true,
sourceMap: true,
target: 'es2022',
externalModules: ['@aws-sdk/*'], // Use AWS SDK v3 from Lambda runtime
},
logRetention: logs.RetentionDays.ONE_MONTH,
tracing: lambda.Tracing.ACTIVE,
});
dataTable.grantReadWriteData(apiHandler);
// HTTP API with JWT authorizer and CORS
const httpApi = new apigateway.HttpApi(this, 'HttpApi', {
apiName: 'serverless-api',
corsPreflight: {
allowOrigins: ['https://app.example.com'],
allowMethods: [apigateway.CorsHttpMethod.GET, apigateway.CorsHttpMethod.POST],
allowHeaders: ['Authorization', 'Content-Type'],
maxAge: cdk.Duration.hours(1),
},
defaultAuthorizer: new apigateway.HttpJwtAuthorizer('JwtAuthorizer',
'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX', {
jwtAudience: ['api-client-id'],
}),
defaultThrottle: {
rateLimit: 1000,
burstLimit: 2000,
},
});
// Route integration with response streaming
httpApi.addRoutes({
path: '/api/v1/resources',
methods: [apigateway.HttpMethod.GET],
integration: new apigateway.HttpLambdaIntegration('GetResources', apiHandler),
});
// CloudWatch dashboard for monitoring
const dashboard = new cdk.aws_cloudwatch.Dashboard(this, 'ApiDashboard', {
dashboardName: 'serverless-api-metrics',
});
dashboard.addWidgets(
new cdk.aws_cloudwatch.GraphWidget({
title: 'API Latency',
left: [apiHandler.metricDuration({ statistic: 'p99' })],
}),
new cdk.aws_cloudwatch.GraphWidget({
title: 'Error Rate',
left: [apiHandler.metricErrors({ statistic: 'sum' })],
})
);
}
}
The Lambda handler implementation demonstrates critical patterns for production reliability:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
const logger = new Logger();
const metrics = new Metrics();
const tracer = new Tracer();
const ddbClient = tracer.captureAWSv3Client(
DynamoDBDocumentClient.from(new DynamoDBClient({}), {
marshallOptions: { removeUndefinedValues: true },
})
);
interface Resource {
id: string;
name: string;
createdAt: string;
metadata: Record<string, unknown>;
}
export const handler = async (
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
const segment = tracer.getSegment();
try {
// Extract and validate request parameters
const userId = event.requestContext.authorizer?.jwt.claims.sub as string;
const limit = Math.min(parseInt(event.queryStringParameters?.limit || '20'), 100);
logger.addContext({ userId, limit });
metrics.addMetadata('userId', userId);
// Query with consistent read for strong consistency
const result = await ddbClient.send(
new QueryCommand({
TableName: process.env.TABLE_NAME,
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
},
Limit: limit,
ConsistentRead: false, // Eventually consistent for better performance
ProjectionExpression: 'id, #name, createdAt, metadata',
ExpressionAttributeNames: {
'#name': 'name', // Reserved keyword handling
},
})
);
const resources: Resource[] = result.Items as Resource[];
metrics.addMetric('ResourcesRetrieved', MetricUnits.Count, resources.length);
logger.info('Resources retrieved successfully', { count: resources.length });
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'private, max-age=60',
},
body: JSON.stringify({
resources,
count: resources.length,
nextToken: result.LastEvaluatedKey ?
Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64') :
undefined,
}),
};
} catch (error) {
logger.error('Error retrieving resources', { error });
metrics.addMetric('ResourceRetrievalError', MetricUnits.Count, 1);
segment?.addError(error as Error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Internal server error',
requestId: event.requestContext.requestId,
}),
};
} finally {
metrics.publishStoredMetrics();
}
};
This implementation addresses several critical production requirements. The Lambda function uses ARM64 architecture for 20% better price-performance. Memory allocation at 1769MB provides optimal CPU allocation without overpaying. The 29-second timeout stays under API Gateway's 30-second limit while allowing complex operations. AWS SDK v3 is excluded from bundling since Lambda runtime provides it, reducing deployment package size by 80%.
Authentication and Authorization Patterns
Modern serverless APIs require sophisticated authentication beyond simple API keys. JWT-based authentication with Cognito, Auth0, or Okta provides stateless verification at the API Gateway layer, eliminating Lambda invocations for unauthorized requests. This reduces costs by 40-60% for public APIs with high bot traffic.
For machine-to-machine communication, implement OAuth 2.0 client credentials flow with short-lived tokens. Store client secrets in AWS Secrets Manager with automatic rotation. Validate token scopes at the Lambda level for fine-grained authorization:
interface JWTClaims {
sub: string;
scope: string;
exp: number;
}
function validateScopes(claims: JWTClaims, requiredScopes: string[]): boolean {
const tokenScopes = claims.scope.split(' ');
return requiredScopes.every(scope => tokenScopes.includes(scope));
}
// In handler
const claims = event.requestContext.authorizer?.jwt.claims as JWTClaims;
if (!validateScopes(claims, ['read:resources', 'write:resources'])) {
return {
statusCode: 403,
body: JSON.stringify({ error: 'Insufficient permissions' }),
};
}
For internal APIs within AWS, use IAM authentication with SigV4 signing. This eliminates token management overhead and provides automatic credential rotation through IAM roles. API Gateway validates signatures before Lambda invocation, providing zero-cost authentication.
Performance Optimization Strategies
Cold start optimization remains critical despite improvements in 2025. Provisioned concurrency eliminates cold starts for predictable traffic but costs $0.015 per GB-hour regardless of invocations. Use it selectively for latency-critical endpoints serving authenticated users during business hours.
For variable traffic, implement tiered warming strategies. Keep 2-3 instances warm during off-peak hours using EventBridge scheduled rules that invoke functions every 5 minutes. Scale provisioned concurrency based on CloudWatch metrics during peak hours. This hybrid approach reduces costs by 70% compared to full provisioned concurrency while maintaining P99 latency under 200ms.
Lambda SnapStart for Java and .NET applications requires specific initialization patterns. Move expensive operations like database connection pool creation and configuration loading into the initialization phase:
// Initialization phase - executed once and snapshotted
private static final DynamoDbClient ddbClient = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.httpClient(UrlConnectionHttpClient.builder().build())
.build();
private static final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Handler method - executed per invocation
public APIGatewayProxyResponseEvent handleRequest(
APIGatewayProxyRequestEvent event, Context context) {
// Fast execution using pre-initialized resources
}
Response caching at API Gateway reduces Lambda invocations by 80-95% for read-heavy APIs. Configure cache TTL based on data freshness requirements. Use cache key parameters to segment cache by user, region, or API version. Implement cache invalidation through API calls when data changes.
Error Handling and Resilience
Production serverless APIs must handle partial failures gracefully. DynamoDB throttling, Lambda concurrency limits, and downstream service timeouts occur regularly at scale. Implement exponential backoff with jitter for retryable errors:
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 100
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}
const jitter = Math.random() * 0.3 + 0.85; // 85-115% of base delay
const delay = baseDelay * Math.pow(2, attempt) * jitter;
logger.warn('Operation failed, retrying', { attempt, delay, error });
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
function isRetryable(error: unknown): boolean {
if (error instanceof Error) {
return ['ThrottlingException', 'ServiceUnavailable', 'RequestTimeout']
.some(code => error.name.includes(code));
}
return false;
}
Implement circuit breakers for downstream service calls to prevent cascade failures. When error rates exceed thresholds, fail fast instead of waiting for timeouts. This preserves Lambda concurrency for healthy requests:
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold: number = 5,
private timeout: number = 60000,
private resetTimeout: number = 30000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
logger.error('Circuit breaker opened', { failures: this.failures });
}
}
}
Cost Optimization Techniques
Serverless costs spiral quickly without proper optimization. A single misconfigured Lambda function processing high-volume events can generate $100,000+ monthly bills. Implement these cost controls immediately.
Right-size Lambda memory allocation through load testing. Memory directly correlates with CPU allocation and execution speed. A function using 512MB that completes in 2 seconds might complete in 800ms at 1024MB. The 2x memory increase costs 2x per millisecond but reduces duration by 60%, resulting in 20% cost savings.
Use API Gateway HTTP APIs instead of REST APIs for new projects. HTTP APIs cost $1.00 per million requests versus $3.50 for REST APIs. They provide equivalent functionality for most use cases with JWT authorizers, CORS, and custom domains. Only use REST APIs when you need API keys, usage plans, or request/response transformation.
Implement request validation at API Gateway to reject malformed requests before Lambda invocation. This prevents wasted Lambda executions and reduces costs by 15-30% for public APIs:
const requestValidator = new apigateway.RequestValidator(this, 'RequestValidator', {
restApi: api,
validateRequestBody: true,
validateRequestParameters: true,
});
const requestModel = api.addModel('RequestModel', {
contentType: 'application/json',
schema: {
type: apigateway.JsonSchemaType.OBJECT,
required: ['name', 'type'],
properties: {
name: { type: apigateway.JsonSchemaType.STRING, minLength: 1, maxLength: 100 },
type: { type: apigateway.JsonSchemaType.STRING, enum: ['A', 'B', 'C'] },
metadata: { type: apigateway.JsonSchemaType.OBJECT },
},
},
});
Monitor Lambda concurrency usage and set reserved concurrency limits to prevent runaway costs. A single function shouldn't consume all account concurrency (1000 by default). Reserve 100-200 concurrency for critical functions and set limits on batch processing functions.
Common Pitfalls and Edge Cases
Teams frequently encounter these failure modes in production. Lambda timeout errors occur when functions exceed 30 seconds but API Gateway already returne