01 — Architecture
The Layer Contract
Six distinct layers, each owned by specific AWS services. Every architectural decision traces back to this stack — from the CDN edge down to the database.
02 — Service Selection
Service Reference
AWS has 200+ services. These are the ones that form a complete, production-grade backend — and exactly when to use each one versus the alternatives.
HTTP APISQS consumerS3 trigger
JWT authorizerWebSocket API
RDS ProxyData APIMulti-AZ
Dead-letter queueFIFOFan-out
JWT authorizerCustom attributes
PutEvents()Scheduler
DynamoDB StreamsGlobal tables
WebSocket serverLong tasks
GetSecretValue()Auto-rotation
cdk deploycdk synth
Serverless modeIAM auth
InvokeModel()Knowledge Bases
03 — Project Structure
Monorepo Layout
Monorepo with separate packages for Lambda functions, CDK infrastructure, and shared types. Esbuild bundles each Lambda independently — small artifact, fast cold start.
04 — Code Patterns
TypeScript Blueprints
Production-ready TypeScript using AWS SDK v3, Drizzle ORM, AWS Lambda Powertools, and CDK v2. Every snippet is copy-paste ready.
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda' import { logger } from '../shared/logger' import { requireAuth } from '../shared/auth.middleware' import { validate } from '../shared/validate' import { ProductService } from './service' import { z } from 'zod' const CreateSchema = z.object({ name: z.string().min(1).max(200), price: z.number().positive(), sku: z.string().min(1), }) // Middy middleware wraps: Powertools tracer + logger export const handler = async ( event: APIGatewayProxyEventV2 ): Promise<APIGatewayProxyResultV2> => { const user = requireAuth(event) if (!user) return { statusCode: 401, body: 'Unauthorized' } const method = event.requestContext.http.method if (method === 'GET') { const products = await ProductService.list(user.sub) return json(200, products) } if (method === 'POST') { const body = validate(CreateSchema, JSON.parse(event.body ?? '{}')) if (!body.success) return json(422, body.error.flatten()) const product = await ProductService.create(body.data, user.sub) return json(201, product) } return json(405, { error: 'Method not allowed' }) } const json = (status: number, body: unknown) => ({ statusCode: status, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), })
import { SQSEvent, SQSBatchResponse } from 'aws-lambda' import { logger } from '../shared/logger' // Partial batch failure — only retry failed messages export const handler = async ( event: SQSEvent ): Promise<SQSBatchResponse> => { const failures: string[] = [] await Promise.allSettled( event.Records.map(async (record) => { try { const msg = JSON.parse(record.body) await processMessage(msg) logger.info('Message processed', { id: record.messageId }) } catch (err) { logger.error('Message failed', { id: record.messageId, err }) // Report partial failure — only this message retries failures.push(record.messageId) } }) ) return { batchItemFailures: failures.map(id => ({ itemIdentifier: id, })), } } async function processMessage(msg: unknown) { // Validate + handle the typed SQS message const parsed = ProductEventSchema.parse(msg) switch (parsed.type) { case 'product.created': return NotificationService.notifySeller(parsed.sellerId) default: logger.warn('Unknown event type', { type: parsed.type }) } }
import { PreSignUpTriggerEvent, PostConfirmationTriggerEvent } from 'aws-lambda' import { CognitoIdentityProviderClient, AdminAddUserToGroupCommand } from '@aws-sdk/client-cognito-identity-provider' const cognito = new CognitoIdentityProviderClient({}) // Block disposable email domains before signup export const preSignUp = async (e: PreSignUpTriggerEvent) => { const banned = ['mailinator.com', 'tempmail.com'] const domain = e.request.userAttributes.email?.split('@')[1] if (banned.includes(domain ?? '')) throw new Error('Email domain not allowed') return e } // Create user profile in RDS after confirmation export const postConfirmation = async ( e: PostConfirmationTriggerEvent ) => { const { sub, email, name } = e.request.userAttributes // Add to 'users' Cognito group (default role) await cognito.send(new AdminAddUserToGroupCommand({ UserPoolId: e.userPoolId, Username: e.userName, GroupName: 'users', })) // Create DB record await UserRepository.create({ id: sub, email, name }) return e }
import { drizzle } from 'drizzle-orm/node-postgres' import { Pool } from 'pg' import { getSecret } from './secrets' import * as schema from './schema' let db: ReturnType<typeof drizzle> | null = null // Lambda execution context reuses the connection pool // RDS Proxy manages the actual PG connections export async function getDb() { if (db) return db const secret = await getSecret('rds/credentials') const creds = JSON.parse(secret) const pool = new Pool({ // Connect to RDS Proxy endpoint — not direct RDS host: process.env.RDS_PROXY_ENDPOINT, user: creds.username, password: creds.password, database: process.env.DB_NAME, ssl: { rejectUnauthorized: true }, max: 5, // Low — Lambda shares via proxy idleTimeoutMillis: 30_000, }) db = drizzle(pool, { schema, logger: false }) return db }
import { pgTable, text, timestamp, pgEnum } from 'drizzle-orm/pg-core' export const roleEnum = pgEnum('user_role', [ 'admin', 'moderator', 'user', ]) export const users = pgTable('users', { id: text('id').primaryKey(), // Cognito sub email: text('email').notNull().unique(), name: text('name').notNull(), role: roleEnum('role').default('user').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export const products = pgTable('products', { id: text('id').primaryKey() .$defaultFn(() => crypto.randomUUID()), name: text('name').notNull(), price: text('price').notNull(), sku: text('sku').notNull().unique(), sellerId: text('seller_id').references(() => users.id), createdAt: timestamp('created_at').defaultNow().notNull(), }) export type User = typeof users.$inferSelect export type NewUser = typeof users.$inferInsert
import { eq, ilike, desc } from 'drizzle-orm' import { getDb } from '../shared/db' import { products } from './schema' import type { NewProduct } from './schema' export class ProductRepository { static async findById(id: string) { const db = await getDb() return db.query.products.findFirst({ where: eq(products.id, id), }) } static async listBySeller( sellerId: string, opts: { search?: string; limit?: number } = {}, ) { const db = await getDb() return db.select().from(products) .where( opts.search ? ilike(products.name, `%${opts.search}%`) : eq(products.sellerId, sellerId) ) .orderBy(desc(products.createdAt)) .limit(opts.limit ?? 20) } static async create(data: NewProduct) { const db = await getDb() const [row] = await db .insert(products) .values(data) .returning() return row } }
import { APIGatewayProxyEventV2 } from 'aws-lambda' import { CognitoJwtVerifier } from 'aws-jwt-verify' const verifier = CognitoJwtVerifier.create({ userPoolId: process.env.USER_POOL_ID!, tokenUse: 'access', clientId: process.env.USER_POOL_CLIENT_ID!, }) export interface AuthContext { sub: string email: string groups: string[] username: string } export async function requireAuth( event: APIGatewayProxyEventV2, ): Promise<AuthContext | null> { const token = event.headers.authorization ?.replace('Bearer ', '') if (!token) return null try { const payload = await verifier.verify(token) return { sub: payload.sub, email: payload.email as string, groups: (payload['cognito:groups'] ?? []) as string[], username: payload.username as string, } } catch { return null } } // RBAC check — Cognito groups = roles export function requireGroup( ctx: AuthContext, ...groups: string[] ): boolean { return groups.some(g => ctx.groups.includes(g)) }
// In ApiStack.ts — attach Cognito authorizer to HTTP API import { HttpUserPoolAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers' import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations' const authorizer = new HttpUserPoolAuthorizer( 'CognitoAuth', userPool, { userPoolClients: [userPoolClient] }, ) // All routes protected by Cognito JWT by default api.addRoutes({ path: '/products', methods: [HttpMethod.GET, HttpMethod.POST], integration: new HttpLambdaIntegration('Products', productsFn), authorizer, }) // Public routes — no authorizer api.addRoutes({ path: '/health', methods: [HttpMethod.GET], integration: new HttpLambdaIntegration('Health', healthFn), }) // Admin routes — Lambda authorizer checks group membership api.addRoutes({ path: '/admin/users', methods: [HttpMethod.GET], integration: new HttpLambdaIntegration('Admin', adminFn), authorizer: lambdaAuthorizer, // custom group check })
import { SQSClient, SendMessageCommand, SendMessageBatchCommand } from '@aws-sdk/client-sqs' const sqs = new SQSClient({ region: process.env.AWS_REGION }) // Type-safe message publisher export async function publishToQueue<T extends object>( queueUrl: string, message: T, opts: { delaySeconds?: number dedupId?: string // FIFO only groupId?: string // FIFO only } = {}, ) { return sqs.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: JSON.stringify(message), DelaySeconds: opts.delaySeconds, MessageDeduplicationId: opts.dedupId, MessageGroupId: opts.groupId, MessageAttributes: { eventType: { DataType: 'String', StringValue: (message as any).type ?? 'unknown', }, }, })) } // In a service: await publishToQueue(process.env.ORDERS_QUEUE_URL!, { type: 'order.placed', orderId: order.id, userId: user.sub, amount: order.total, })
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns' import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge' const sns = new SNSClient({}) const eb = new EventBridgeClient({}) // SNS — fan-out to multiple SQS queues export async function publishToTopic( topicArn: string, message: unknown, subject?: string, ) { return sns.send(new PublishCommand({ TopicArn: topicArn, Message: JSON.stringify(message), Subject: subject, })) } // EventBridge — domain events, cross-account routing export async function putDomainEvent<T>( source: string, detailType: string, detail: T, ) { return eb.send(new PutEventsCommand({ Entries: [{ Source: source, DetailType: detailType, Detail: JSON.stringify(detail), EventBusName: process.env.EVENT_BUS_NAME, }], })) } // Example usage: // await putDomainEvent('myapp.orders', 'OrderPlaced', { orderId, ... }) // EventBridge rule routes to Lambda, SQS, or Step Functions
import { createClient } from 'redis' let client: ReturnType<typeof createClient> | null = null async function getClient() { if (client?.isReady) return client client = createClient({ // ElastiCache Serverless endpoint url: `rediss://${process.env.CACHE_ENDPOINT}:6379`, socket: { tls: true, rejectUnauthorized: true }, }) await client.connect() return client } // Generic typed cache-aside helper export async function cached<T>( key: string, ttlSec: number, fetcher: () => Promise<T>, ): Promise<T> { const c = await getClient() const hit = await c.get(key) if (hit) return JSON.parse(hit) as T const value = await fetcher() await c.setEx(key, ttlSec, JSON.stringify(value)) return value } // Rate limiting with Redis INCR + EXPIRE export async function checkRateLimit( key: string, maxRequests: number, windowSec: number, ): Promise<boolean> { const c = await getClient() const count = await c.incr(key) if (count === 1) await c.expire(key, windowSec) return count <= maxRequests }
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager' const sm = new SecretsManagerClient({}) const cache = new Map<string, string>() export async function getSecret(name: string): Promise<string> { // Cache in Lambda execution context (warm instances) if (cache.has(name)) return cache.get(name)! const { SecretString } = await sm.send( new GetSecretValueCommand({ SecretId: name }) ) if (!SecretString) throw new Error(`Secret ${name} is empty`) cache.set(name, SecretString) return SecretString } // Or use the Lambda Extension for zero-latency secrets: // Set env vars at function level — extension fetches them // before your handler runs and caches for 5 minutes. // // In CDK: // fn.addEnvironment('MY_SECRET', // Secret.fromSecretsManager(mySecret).secretValue.toString()) // // Then: process.env.MY_SECRET — no SDK call needed.
import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime' const bedrock = new BedrockRuntimeClient({ region: 'us-east-1', // Bedrock model availability varies by region }) export class BedrockService { // Claude 3.5 Sonnet via Bedrock — IAM auth, no API key static async generate( prompt: string, maxTokens = 2048, ): Promise<string> { const { body } = await bedrock.send( new InvokeModelCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', contentType: 'application/json', accept: 'application/json', body: JSON.stringify({ anthropic_version: 'bedrock-2023-05-31', max_tokens: maxTokens, messages: [{ role: 'user', content: prompt }], }), }) ) const res = JSON.parse(new TextDecoder().decode(body)) return res.content[0].text } // Streaming — for Lambda response streaming or SSE static async* stream(prompt: string) { const { body } = await bedrock.send( new InvokeModelWithResponseStreamCommand({ modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', contentType: 'application/json', body: JSON.stringify({ anthropic_version: 'bedrock-2023-05-31', max_tokens: 4096, messages: [{ role: 'user', content: prompt }], stream: true, }), }) ) for await (const chunk of body!) { const parsed = JSON.parse( new TextDecoder().decode(chunk.chunk?.bytes) ) if (parsed.type === 'content_block_delta') yield parsed.delta?.text ?? '' } } }
import * as cdk from 'aws-cdk-lib' import * as lambda from 'aws-cdk-lib/aws-lambda' import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2' import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' export class ApiStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: ApiProps) { super(scope, id, props) const productsFn = new NodejsFunction(this, 'ProductsFn', { entry: 'packages/functions/src/features/products/handler.ts', handler: 'handler', runtime: lambda.Runtime.NODEJS_20_X, architecture:lambda.Architecture.ARM_64, // Graviton — cheaper memorySize: 512, timeout: cdk.Duration.seconds(29), vpc: props.vpc, // must be in VPC for RDS vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, environment: { RDS_PROXY_ENDPOINT: props.rdsProxy.endpoint, DB_NAME: 'myapp', CACHE_ENDPOINT: props.cacheEndpoint, }, bundling: { minify: true, sourceMap: true, externalModules: [] }, }) // Grant least-privilege access to Secrets Manager props.dbSecret.grantRead(productsFn) const api = new apigwv2.HttpApi(this, 'Api', { corsPreflight: { allowOrigins: ['https://myapp.com'], allowMethods: [apigwv2.CorsHttpMethod.ANY], allowHeaders: ['Authorization', 'Content-Type'], }, }) new cdk.CfnOutput(this, 'ApiUrl', { value: api.url! }) } }
import * as rds from 'aws-cdk-lib/aws-rds' import * as ec2 from 'aws-cdk-lib/aws-ec2' export class DatabaseStack extends cdk.Stack { public readonly cluster: rds.DatabaseCluster public readonly proxy: rds.DatabaseProxy public readonly secret: secretsmanager.ISecret constructor(scope: cdk.App, id: string, props: DbProps) { super(scope, id, props) this.cluster = new rds.DatabaseCluster(this, 'Cluster', { engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_1, }), serverlessV2MinCapacity: 0.5, serverlessV2MaxCapacity: 128, writer: rds.ClusterInstance.serverlessV2('writer', { publiclyAccessible: false, }), readers: [ rds.ClusterInstance.serverlessV2('reader', { scaleWithWriter: true, }), ], vpc: props.vpc, storageEncrypted: true, // KMS encryption deletionProtection: true, // prod safeguard backup: { retention: cdk.Duration.days(7) }, }) // RDS Proxy — connection pooling for Lambda this.proxy = this.cluster.addProxy('Proxy', { secrets: [this.cluster.secret!], vpc: props.vpc, requireTLS: true, iamAuth: true, // IAM instead of password }) this.secret = this.cluster.secret! } }
import * as sqs from 'aws-cdk-lib/aws-sqs' import * as sns from 'aws-cdk-lib/aws-sns' import * as sub from 'aws-cdk-lib/aws-sns-subscriptions' import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources' export class MessagingStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps) { super(scope, id, props) // DLQ — catches failed messages after 3 retries const dlq = new sqs.Queue(this, 'DLQ', { retentionPeriod: cdk.Duration.days(14), encryption: sqs.QueueEncryption.KMS_MANAGED, }) const ordersQueue = new sqs.Queue(this, 'OrdersQueue', { visibilityTimeout: cdk.Duration.seconds(30), encryption: sqs.QueueEncryption.KMS_MANAGED, deadLetterQueue: { queue: dlq, maxReceiveCount: 3 }, }) // SNS topic → fan-out to multiple queues const ordersTopic = new sns.Topic(this, 'OrdersTopic') ordersTopic.addSubscription(new sub.SqsSubscription(ordersQueue)) // Wire consumer Lambda to SQS with partial batch failure consumerFn.addEventSource(new SqsEventSource(ordersQueue, { batchSize: 10, maxBatchingWindow: cdk.Duration.seconds(5), reportBatchItemFailures: true, // partial failure maxConcurrency: 50, })) } }
import { Logger } from '@aws-lambda-powertools/logger' import { Tracer } from '@aws-lambda-powertools/tracer' import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics' // Lambda Powertools — structured logging, tracing, metrics // All auto-correlated via X-Ray trace ID export const logger = new Logger({ serviceName: process.env.SERVICE_NAME ?? 'api', logLevel: process.env.LOG_LEVEL as any ?? 'INFO', }) export const tracer = new Tracer({ serviceName: process.env.SERVICE_NAME ?? 'api', captureHTTPsRequests: true, }) export const metrics = new Metrics({ namespace: 'MyApp', serviceName: process.env.SERVICE_NAME ?? 'api', }) // Usage in handler: // logger.info('Order placed', { orderId, userId }) // tracer.getSegment()?.addAnnotation('orderId', orderId) // metrics.addMetric('OrdersPlaced', MetricUnit.Count, 1) // drizzle-kit scripts: // "db:generate": "drizzle-kit generate" // "db:migrate": "drizzle-kit migrate" // "db:studio": "drizzle-kit studio" // "deploy": "cdk deploy --all" // "test": "vitest run"
05 — Data Flows
Every Request Traced
From CloudFront edge to RDS row — every hop across managed services, with the AWS service at each step.
Authenticated HTTP request → RDS
Order placed → async processing
Cache-aside with ElastiCache
User registration flow
EventBridge scheduled job
Bedrock AI streaming response
SQS DLQ + retry
06 — Feature Matrix
Every Requirement Covered
All backend requirements mapped to AWS-idiomatic managed services — nothing to operate, everything managed.
aws-lambda
aws-jwt-verify
drizzle-orm
pg
@aws-sdk/client-sqs
redis
@aws-sdk/client-secrets-manager
@aws-sdk/client-eventbridge
CDK WAFv2Construct
@aws-sdk/client-bedrock-runtime
API Gateway WS API
@aws-lambda-powertools/logger
aws-cdk-lib
07 — SDK Registry
The npm Registry
Pinned to production-stable versions. AWS SDK v3 — modular, tree-shakable, smaller Lambda bundles than v2.
@aws-sdk/client-sqs, not the monolithic v2 aws-sdk. Each client is a separate package — esbuild bundles only the code your Lambda actually uses. Average Lambda bundle drops from 8MB to under 500KB.
| Package | Version | Purpose | Layer |
|---|---|---|---|
aws-lambda | types | TypeScript types for all Lambda event types — APIGatewayProxyEventV2, SQSEvent, S3Event, etc. | Lambda |
@aws-sdk/client-cognito-identity-provider | ^3.x | Admin operations — AdminAddUserToGroup, AdminDisableUser, ListUsersInGroup | Auth |
aws-jwt-verify | ^4.x | Cognito JWT verification in Lambda — CognitoJwtVerifier with JWKS caching | Auth |
@aws-sdk/client-rds-data | ^3.x | RDS Data API — HTTP-based SQL without connection pooling (alternative to RDS Proxy) | RDS |
drizzle-orm | ^0.41 | Type-safe ORM for PostgreSQL — schema definition, typed queries, migrations via drizzle-kit | RDS |
pg | ^8.13 | PostgreSQL driver — used by Drizzle, connects via RDS Proxy | RDS |
drizzle-kit | ^0.30 | Migration generator, schema introspection, Drizzle Studio GUI | dev |
@aws-sdk/client-sqs | ^3.x | SQS send, receive, delete, batch operations | SQS |
@aws-sdk/client-sns | ^3.x | SNS publish, topic management, subscriptions | SNS |
@aws-sdk/client-eventbridge | ^3.x | PutEvents for domain event publishing, manage rules and targets | Events |
@aws-sdk/client-secrets-manager | ^3.x | GetSecretValue — fetch credentials, API keys, JWT secrets at runtime | Secrets |
@aws-sdk/client-s3 | ^3.x | S3 object CRUD, presigned URLs for direct client upload/download | Storage |
@aws-sdk/client-bedrock-runtime | ^3.x | InvokeModel, InvokeModelWithResponseStream — Claude, Llama, Titan via Bedrock | AI |
redis | ^4.7 | ElastiCache Redis client — get/set/incr/expire, pub/sub, pipeline | Cache |
@aws-lambda-powertools/logger | ^2.x | Structured CloudWatch Logs — JSON, log levels, correlation IDs, sampling | Ops |
@aws-lambda-powertools/tracer | ^2.x | AWS X-Ray distributed tracing — annotate segments, capture HTTP calls | Ops |
@aws-lambda-powertools/metrics | ^2.x | Custom CloudWatch metrics — business KPIs, SLI/SLO tracking | Ops |
aws-cdk-lib | ^2.170 | CDK v2 — all AWS constructs in one package, L1/L2/L3 constructs | IaC |
zod | ^3.23 | Runtime schema validation — request body, SQS message shapes, environment variables | Shared |
vitest | ^2.x | Unit tests — Lambda handlers and services via AWS SDK mocking (aws-sdk-client-mock) | test |
aws-sdk-client-mock | ^4.x | Mock AWS SDK v3 clients in tests — mockClient(SQSClient).on(SendMessageCommand) | test |
| REPLACED BY AWS MANAGED SERVICES: JWT library → Cognito + aws-jwt-verify · Redis server → ElastiCache Serverless · Celery/workers → SQS Lambda triggers · Nginx → CloudFront + API Gateway · Cron daemon → EventBridge Scheduler · Database server → RDS Aurora Serverless v2 · Secret storage → Secrets Manager · Auth service → Cognito User Pools | |||
✦ — AI Scaffold
Generate with AI
Paste into Claude, ChatGPT, Gemini, or any AI agent to scaffold this exact architecture from scratch.
You are a senior AWS cloud architect. Scaffold a production-ready serverless backend. Stack: - Runtime: AWS Lambda (Node.js 20 / TypeScript) - API: Amazon API Gateway HTTP API (v2) - Auth: Amazon Cognito User Pools + aws-jwt-verify - Database: Amazon RDS Aurora Serverless v2 (PostgreSQL) via RDS Proxy - ORM: Drizzle ORM with drizzle-kit migrations - Queue: Amazon SQS (FIFO) + Lambda event source mapping - Events: Amazon EventBridge custom bus - Storage: Amazon S3 + CloudFront - Cache: Amazon ElastiCache Serverless (Redis 7) - Secrets: AWS Secrets Manager - IaC: AWS CDK v2 (TypeScript) - Observability: Lambda Powertools (Logger, Tracer, Metrics) - AI: Amazon Bedrock (Claude claude-sonnet-4-6) Provide: 1. CDK stack definitions (ApiStack, AuthStack, DatabaseStack, QueueStack) 2. Lambda handler structure with Powertools decorators 3. Drizzle schema + migration pattern 4. IAM least-privilege role per Lambda 5. SQS consumer with dead-letter queue 6. EventBridge publish/subscribe pattern 7. GitHub Actions CI/CD pipeline (test → cdk diff → cdk deploy) 8. Environment variable management with Secrets Manager