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

Sanjeev SharmaSanjeev Sharma
5 min read

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

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

ServiceFree TierCost Saver
Lambda1M requests/monthPay per invocation
S35GB + 20K requestsUse CDN, compress assets
RDS750h t2.microRight-size instances
EC2750h t2.microReserved instances (40% off)
CloudFront1TB/monthCaches 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro