AWS for Developers 2026: EC2, S3, Lambda, RDS, and CloudFront Guide
Advertisement
AWS for Developers 2026: The Services You Actually Need
AWS has 200+ services. As a developer, you need about 10. This guide focuses on what you'll use in production.
- The Developer's AWS Starter Kit
- S3: File Storage for Everything
- Lambda: Serverless Functions
- RDS: Managed PostgreSQL
- CloudFront CDN
- AWS CDK: Infrastructure as Code
- AWS Cost Optimization
The Developer's AWS Starter Kit
Compute: EC2 (servers), Lambda (serverless), ECS (containers)
Storage: S3 (files/objects), EBS (block), EFS (shared filesystem)
Database: RDS (PostgreSQL/MySQL), DynamoDB (NoSQL), ElastiCache (Redis)
Network: VPC, CloudFront (CDN), Route 53 (DNS), ALB (load balancer)
Auth: Cognito (user pools), IAM (service permissions)
Queue: SQS, SNS, EventBridge
Developer: CloudWatch (logs/metrics), CodeDeploy, Secrets Manager
S3: File Storage for Everything
// Upload files to S3
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
const BUCKET = process.env.S3_BUCKET!
// Upload a file
export async function uploadFile(
key: string,
body: Buffer | Blob | ReadableStream,
contentType: string
): Promise<string> {
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: body as any,
ContentType: contentType,
CacheControl: 'public, max-age=31536000', // 1 year for immutable assets
}))
return `https://${BUCKET}.s3.amazonaws.com/${key}`
}
// Generate presigned URL for direct browser upload (avoids proxying through server)
export async function getUploadUrl(key: string, contentType: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
ContentType: contentType,
})
return getSignedUrl(s3, command, { expiresIn: 3600 }) // 1 hour
}
// Generate presigned URL for private file download
export async function getDownloadUrl(key: string, expiresInSeconds = 3600): Promise<string> {
const command = new GetObjectCommand({ Bucket: BUCKET, Key: key })
return getSignedUrl(s3, command, { expiresIn: expiresInSeconds })
}
// Next.js API route for image upload
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getUploadUrl } from '@/lib/s3'
import { auth } from '@/auth'
import { nanoid } from 'nanoid'
export async function POST(request: NextRequest) {
const session = await auth()
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { contentType, fileName } = await request.json()
const ext = fileName.split('.').pop()
const key = `uploads/${session.user.id}/${nanoid()}.${ext}`
const uploadUrl = await getUploadUrl(key, contentType)
return NextResponse.json({ uploadUrl, key, publicUrl: `${process.env.CDN_URL}/${key}` })
}
Lambda: Serverless Functions
// handler.ts — Lambda function
import type { APIGatewayProxyHandler } from 'aws-lambda'
export const handler: APIGatewayProxyHandler = async (event) => {
const method = event.httpMethod
const path = event.path
const body = event.body ? JSON.parse(event.body) : null
try {
if (method === 'GET' && path === '/users') {
const users = await getUsers()
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify(users),
}
}
return { statusCode: 404, body: JSON.stringify({ error: 'Not found' }) }
} catch (error) {
return { statusCode: 500, body: JSON.stringify({ error: 'Internal error' }) }
}
}
# serverless.yml — Deploy with Serverless Framework
service: my-api
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
environment:
DATABASE_URL: ${ssm:/myapp/database-url}
functions:
api:
handler: dist/handler.handler
events:
- httpApi:
path: /{proxy+}
method: ANY
timeout: 30
memorySize: 512
RDS: Managed PostgreSQL
// Connect to RDS PostgreSQL
import { Pool } from 'pg'
const pool = new Pool({
host: process.env.RDS_ENDPOINT,
port: 5432,
database: 'myapp',
user: process.env.RDS_USERNAME,
password: process.env.RDS_PASSWORD,
ssl: {
require: true,
rejectUnauthorized: false, // For RDS
},
max: 10, // Connection pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
})
# Terraform: Create RDS instance
resource "aws_db_instance" "postgres" {
identifier = "myapp-db"
engine = "postgres"
engine_version = "16.2"
instance_class = "db.t3.medium"
allocated_storage = 20
storage_encrypted = true
db_name = "myapp"
username = "dbadmin"
password = var.db_password
multi_az = true # High availability
deletion_protection = true
skip_final_snapshot = false
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 7 # 7 days of automated backups
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
}
CloudFront CDN
// Serve S3 assets via CloudFront
// CloudFront URL: https://d1234567890.cloudfront.net/image.jpg
// Instead of: https://mybucket.s3.amazonaws.com/image.jpg
// Benefits: 450+ edge locations, 10-100x faster, HTTPS termination, WAF
// CloudFront invalidation after deploy
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront'
const cf = new CloudFrontClient({ region: 'us-east-1' })
async function invalidateCache(paths: string[]): Promise<void> {
await cf.send(new CreateInvalidationCommand({
DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID!,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths, // ['/*'] for full invalidation
},
},
}))
}
AWS CDK: Infrastructure as Code
// cdk/app.ts — Define infrastructure in TypeScript
import * as cdk from 'aws-cdk-lib'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as ecs from 'aws-cdk-lib/aws-ecs'
import * as rds from 'aws-cdk-lib/aws-rds'
class AppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// VPC
const vpc = new ec2.Vpc(this, 'AppVPC', { maxAzs: 2 })
// ECS Cluster
const cluster = new ecs.Cluster(this, 'AppCluster', { vpc })
// Fargate Service (serverless containers)
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 256, memoryLimitMiB: 512,
})
taskDef.addContainer('App', {
image: ecs.ContainerImage.fromRegistry('ghcr.io/myapp:latest'),
portMappings: [{ containerPort: 3000 }],
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'myapp' }),
})
new ecs.FargateService(this, 'AppService', {
cluster, taskDefinition: taskDef,
desiredCount: 2,
assignPublicIp: false,
})
// RDS PostgreSQL
const db = new rds.DatabaseInstance(this, 'AppDB', {
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_16 }),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
vpc,
multiAz: true,
storageEncrypted: true,
deletionProtection: true,
})
}
}
const app = new cdk.App()
new AppStack(app, 'MyAppStack', { env: { region: 'us-east-1' } })
AWS Cost Optimization
| Service | Free Tier | Cost Saver |
|---|---|---|
| Lambda | 1M requests/month | Pay per invocation |
| S3 | 5GB + 20K requests | Use CDN, compress assets |
| RDS | 750h t2.micro | Right-size instances |
| EC2 | 750h t2.micro | Reserved instances (40% off) |
| CloudFront | 1TB/month | Caches origin requests |
Biggest cost mistakes: Leaving NAT Gateways running ($32/month each), oversized RDS instances, and transfer costs from large S3 responses not going through CloudFront.
Advertisement