AWS for Developers 2026 — EC2, S3, Lambda, RDS, and CloudFront Guide

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

AWS has 200+ services, but as a developer you need about 10. In 2026, the core developer toolkit is: S3 for files, Lambda for serverless functions, RDS for databases, CloudFront as a CDN, and CDK for infrastructure as code. This guide focuses on practical patterns you will use in every production project.

The Developer AWS Starter Kit

CategoryServiceUse Case
ComputeEC2, Lambda, ECS FargateServers, serverless, containers
StorageS3, EBS, EFSFiles, block storage, shared filesystem
DatabaseRDS, DynamoDB, ElastiCacheSQL, NoSQL, Redis
NetworkCloudFront, Route 53, ALBCDN, DNS, load balancing
AuthCognito, IAMUser pools, service permissions
QueueSQS, SNS, EventBridgeAsync messaging, events

S3: File Storage for Everything

import { S3Client, PutObjectCommand, GetObjectCommand } 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' })
const BUCKET = process.env.S3_BUCKET!
 
// Upload a file
export async function uploadFile(
  key: string,
  body: Buffer,
  contentType: string
): Promise<string> {
  await s3.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    Body: body,
    ContentType: contentType,
    CacheControl: 'public, max-age=31536000',
  }))
  return `https://${BUCKET}.s3.amazonaws.com/${key}`
}
 
// Generate presigned URL for direct browser upload
export async function getUploadUrl(key: string, contentType: string): Promise<string> {
  return getSignedUrl(
    s3,
    new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: contentType }),
    { expiresIn: 3600 }
  )
}
 
// Generate presigned URL for private file download
export async function getDownloadUrl(key: string, expiresIn = 3600): Promise<string> {
  return getSignedUrl(
    s3,
    new GetObjectCommand({ Bucket: BUCKET, Key: key }),
    { expiresIn }
  )
}

Lambda: Serverless API Handler

// handler.ts — Typed Lambda HTTP handler
import type { APIGatewayProxyHandler } from 'aws-lambda'
 
export const handler: APIGatewayProxyHandler = async (event) => {
  const method = event.httpMethod
  const path = event.path
 
  const headers = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
  }
 
  try {
    if (method === 'GET' && path === '/users') {
      const users = await getUsers()
      return { statusCode: 200, headers, body: JSON.stringify(users) }
    }
 
    if (method === 'POST' && path === '/users') {
      const body = event.body ? JSON.parse(event.body) : {}
      const user = await createUser(body)
      return { statusCode: 201, headers, body: JSON.stringify(user) }
    }
 
    return { statusCode: 404, headers, body: JSON.stringify({ error: 'Not found' }) }
  } catch (error) {
    console.error('Handler error:', error)
    return { statusCode: 500, headers, 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
  architecture: arm64
  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

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 },
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
})

Provision RDS with Terraform:

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
  deletion_protection    = true
  skip_final_snapshot    = false
  backup_retention_period = 7
 
  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
}

CloudFront CDN

CloudFront serves S3 files from 450+ edge locations worldwide — 10-100x faster than serving directly from S3:

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 },
    },
  }))
}
 
// Invalidate everything after a deploy
await invalidateCache(['/*'])

AWS CDK: Infrastructure as TypeScript

// cdk/stack.ts
import * as cdk from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import * as rds from 'aws-cdk-lib/aws-rds'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
 
class AppStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
 
    const vpc = new ec2.Vpc(this, 'AppVPC', { maxAzs: 2 })
 
    const assetsBucket = new s3.Bucket(this, 'Assets', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      versioned: true,
    })
 
    const distribution = new cloudfront.Distribution(this, 'CDN', {
      defaultBehavior: {
        origin: new origins.S3Origin(assetsBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
      },
    })
 
    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,
    })
  }
}

Common Mistakes

  • Leaving NAT Gateways running in unused VPCs — costs $32/month each even with zero traffic
  • Serving S3 directly without CloudFront — slower and generates unnecessary data-transfer costs
  • Oversized RDS instances — use AWS Compute Optimizer to right-size; most teams start on db.t3.micro then scale
  • No multi-AZ for production RDS — a single AZ failure will cause downtime without it
  • Hardcoded AWS credentials — always use IAM roles for EC2/Lambda/ECS and Secrets Manager for application secrets

Best Practices

  • Use IAM roles attached to compute resources (not access keys) for all AWS API calls
  • Store secrets in AWS Secrets Manager and rotate them automatically
  • Enable CloudTrail and GuardDuty in every account for audit and threat detection
  • Use S3 Versioning and lifecycle policies to manage backup storage costs automatically
  • Set AWS Budget alerts at 80% and 100% to catch runaway costs early

Key Takeaways

  • S3 presigned URLs let browsers upload directly to S3 without routing files through your server, saving bandwidth and compute cost
  • Lambda on arm64 architecture is 20% cheaper than x86 and has similar or faster cold-start performance
  • RDS Multi-AZ gives automatic failover in under 60 seconds; enable it for all production databases
  • CloudFront caches S3 content at 450+ edge locations, reducing latency from hundreds of milliseconds to single-digit milliseconds for most users
  • AWS CDK lets you define infrastructure in TypeScript with full type safety, code reuse, and testing capabilities
  • Free tier covers 1M Lambda requests/month, 5GB S3 storage, 750 hours t2.micro EC2, and 750 hours RDS t2.micro per month
  • Always tag every AWS resource with Environment, Team, and Project tags to enable cost allocation reporting
  • Use Secrets Manager instead of hardcoded environment variables — it handles rotation automatically

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading