DEV Community

Cover image for From Zero to Production: The Complete AWS Serverless Full-Stack Journey

From Zero to Production: The Complete AWS Serverless Full-Stack Journey

Table of Contents


Introduction

Quick Links:

We've come a long way, haven't we?

In Part 1, we launched a static website with S3 and CloudFront.
In Part 2, we built a REST API with API Gateway and Lambda.
In Part 3, we added data persistence with DynamoDB.

But here's the thing – they were separate pieces. Three different stacks. Three deployments. What if I told you we could combine everything into a single, elegant, production-ready deployment?

One command. One stack. Complete application.

This is the grand finale of our Serverless Web Mastery series. Today, we're going to:

  1. Combine all components into a single CDK stack
  2. Configure CloudFront to route between frontend and API
  3. Implement production-ready patterns
  4. Deploy everything with one command
  5. Discuss monitoring, security, and cost optimization

Let's ship this thing to production! 🚀


What We've Built

By the end of this tutorial, you'll have:

┌─────────────────────────────────────────────────────────────┐
│               Contact Form for Lead Generation              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Production HTTPS website via CloudFront                    │
│  REST API with full CRUD operations                         │
│  Persistent data storage in DynamoDB                        │
│  Automatic scaling from 0 to millions of users              │
│  Infrastructure as Code with AWS CDK                        │
│  ~$0-2/month for small to medium traffic                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Features

For Users:

  • Beautiful, responsive contact form
  • Instant form submission
  • Success/error feedback
  • Mobile-friendly design

For Developers:

  • One-command deployment
  • Environment-agnostic configuration
  • Structured logging
  • Health check endpoints

For Admins:

  • Query leads by email, status, or ID
  • Update lead status (new → contacted → converted)
  • Delete processed leads
  • View all leads with filters

Full Stack Architecture

Here's our complete architecture:

                         ┌──────────────────┐
                         │      Users       │
                         └────────┬─────────┘
                                  │
                                  ▼
                    ┌──────────────────────────┐
                    │       CloudFront         │
                    │    (HTTPS, Caching)      │
                    └──────────────────────────┘
                                  │
            ┌─────────────────────┼─────────────────────┐
            │                     │                     │
      /static/* ──────────────┐   │   ┌────────────── /api/*
            │                 │   │   │               │
            ▼                 │   │   │               ▼
    ┌───────────────┐         │   │   │       ┌───────────────┐
    │   S3 Bucket   │         │   │   │       │  API Gateway  │
    │               │         │   │   │       │   (REST)      │
    │  index.html   │         │   │   │       └───────┬───────┘
    │  styles.css   │         │   │   │               │
    │  app.js       │         │   │   │               ▼
    └───────────────┘         │   │   │       ┌───────────────┐
                              │   │   │       │    Lambda     │
                              │   │   │       │   Functions   │
                              │   │   │       └───────┬───────┘
                              │   │   │               │
                              │   │   │               ▼
                              │   │   │       ┌───────────────┐
                              │   │   │       │   DynamoDB    │
                              │   │   │       │    Table      │
                              │   │   │       └───────────────┘
                              │   │   │
                              └───┴───┘
Enter fullscreen mode Exit fullscreen mode

Why CloudFront for Both?

Traditional approach:

  • Frontend: https://d1234.cloudfront.net
  • API: https://abc123.execute-api.amazonaws.com
  • Problem: CORS headaches, separate deployments, inconsistent caching

Our approach:

  • Frontend: https://d1234.cloudfront.net/*
  • API: https://d1234.cloudfront.net/api/*
  • Benefits: Same domain = no CORS, unified caching, simpler code

The Complete CDK Stack

Here's our unified stack:

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 s3deploy from "aws-cdk-lib/aws-s3-deployment";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import { Construct } from "constructs";

export class FullIntegrationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ============================================
    // Data Layer
    // ============================================
    const leadsTable = new dynamodb.Table(this, "LeadsTable", {
      tableName: "ContactFormLeads-Prod",
      partitionKey: { name: "leadId", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      pointInTimeRecovery: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN, // Keep data!
    });

    // GSIs for query patterns
    leadsTable.addGlobalSecondaryIndex({
      indexName: "email-index",
      partitionKey: { name: "email", type: dynamodb.AttributeType.STRING },
      sortKey: { name: "createdAt", type: dynamodb.AttributeType.STRING },
    });

    // ============================================
    // API Layer
    // ============================================
    const leadsHandler = new lambdaNodejs.NodejsFunction(this, "LeadsHandler", {
      runtime: lambda.Runtime.NODEJS_22_X,
      entry: path.join(__dirname, "../../lambda/handlers/leads.ts"),
      handler: "handler",
      description: "Leads CRUD operations with DynamoDB",
      environment: {
        TABLE_NAME: leadsTable.tableName,
      },
      bundling: {
        minify: true,
        sourceMap: true,
      },
    });

    leadsTable.grantReadWriteData(leadsHandler);

    const api = new apigateway.RestApi(this, "Api", {
      restApiName: "Contact Form API",
      deployOptions: { stageName: "v1" },
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
      },
    });

    const apiResource = api.root.addResource("api");
    const leads = apiResource.addResource("leads");
    leads.addMethod(
      "POST",
      new apigateway.LambdaIntegration(leadsHandler, { proxy: true }),
    );
    leads.addMethod(
      "GET",
      new apigateway.LambdaIntegration(leadsHandler, { proxy: true }),
    );

    // ============================================
    // Frontend Layer
    // ============================================
    const websiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // ============================================
    // CloudFront: Unified Entry Point
    // ============================================
    const distribution = new cloudfront.Distribution(this, "Distribution", {
      defaultRootObject: "index.html",

      // Default: S3 static files
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(websiteBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
      },

      // /api/*: Route to API Gateway
      additionalBehaviors: {
        "/api/*": {
          origin: new origins.HttpOrigin(
            `${api.restApiId}.execute-api.${this.region}.amazonaws.com`,
            { originPath: "/v1" },
          ),
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
          cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
          originRequestPolicy:
            cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
          allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
        },
      },
    });

    // Deploy frontend
    new s3deploy.BucketDeployment(this, "Deploy", {
      sources: [s3deploy.Source.asset("../frontend")],
      destinationBucket: websiteBucket,
      distribution,
      distributionPaths: ["/*"],
    });

    // Outputs
    new cdk.CfnOutput(this, "URL", {
      value: `https://${distribution.domainName}`,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Insight: CloudFront Path Routing

The magic is in additionalBehaviors:

additionalBehaviors: {
  '/api/*': {
    origin: new origins.HttpOrigin(
      `${api.restApiId}.execute-api.${this.region}.amazonaws.com`,
      { originPath: '/v1' }
    ),
    // ...
  },
},
Enter fullscreen mode Exit fullscreen mode

This routes:

  • https://cloudfronturl.com/api/leadshttps://apigateway/v1/leads

The frontend simply calls /api/leads – no hard-coded URLs!


Frontend Integration

Smart API Configuration

const CONFIG = {
  // Automatically detect environment
  API_ENDPOINT:
    window.location.hostname === "localhost"
      ? "http://localhost:3000" // Local dev
      : "/api", // Production
};
Enter fullscreen mode Exit fullscreen mode

Retry with Exponential Backoff

async function submitToAPI(formData, attempt = 1) {
  try {
    const response = await fetch(`${CONFIG.API_ENDPOINT}/leads`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(formData),
    });

    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  } catch (error) {
    if (attempt < 3) {
      // Exponential backoff: 1s, 2s, 4s
      await delay(1000 * Math.pow(2, attempt - 1));
      return submitToAPI(formData, attempt + 1);
    }
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

One-Command Deployment

cd part-4-full-integration/cdk
npm install
npx cdk deploy
Enter fullscreen mode Exit fullscreen mode

That's it. One command. Everything deployed.

Output:

✅  ContactFormFullStack

Outputs:
ContactFormFullStack.WebsiteURL = https://d1234567abcdef.cloudfront.net
ContactFormFullStack.ApiEndpoint = https://abc123xyz.execute-api.us-east-1.amazonaws.com/v1/
ContactFormFullStack.TableName = ContactFormLeads-Prod
Enter fullscreen mode Exit fullscreen mode

Open the WebsiteURL – your application is live! 🎉


Production Hardening

Before going live, consider these enhancements:

1. Custom Domain

import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';

// Create certificate in us-east-1
const certificate = new acm.Certificate(this, 'Cert', {
  domainName: 'contact.yourdomain.com',
  validation: acm.CertificateValidation.fromDns(hostedZone),
});

// Add to distribution
domainNames: ['contact.yourdomain.com'],
certificate,
Enter fullscreen mode Exit fullscreen mode

2. WAF Protection

import * as wafv2 from "aws-cdk-lib/aws-wafv2";

const webAcl = new wafv2.CfnWebACL(this, "WebAcl", {
  scope: "CLOUDFRONT",
  defaultAction: { allow: {} },
  rules: [
    {
      name: "RateLimitRule",
      priority: 1,
      action: { block: {} },
      statement: {
        rateBasedStatement: {
          limit: 1000, // requests per 5 minutes
          aggregateKeyType: "IP",
        },
      },
      visibilityConfig: {
        /* ... */
      },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

3. Specific CORS Origins

defaultCorsPreflightOptions: {
  allowOrigins: ['https://contact.yourdomain.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
},
Enter fullscreen mode Exit fullscreen mode

4. Lambda Security

// Principle of least privilege
leadsTable.grantReadWriteData(leadsHandler); // ✅ Specific table
// NOT: grantFullAccess()  // ❌ Too broad
Enter fullscreen mode Exit fullscreen mode

Monitoring & Observability

CloudWatch Dashboard

import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";

const dashboard = new cloudwatch.Dashboard(this, "Dashboard", {
  dashboardName: "ContactForm-Production",
});

dashboard.addWidgets(
  new cloudwatch.GraphWidget({
    title: "API Requests",
    left: [api.metricCount()],
  }),
  new cloudwatch.GraphWidget({
    title: "Lambda Duration",
    left: [leadsHandler.metricDuration()],
  }),
  new cloudwatch.GraphWidget({
    title: "DynamoDB Throttles",
    left: [leadsTable.metricThrottledRequests()],
  }),
);
Enter fullscreen mode Exit fullscreen mode

Alarms

new cloudwatch.Alarm(this, "ErrorAlarm", {
  metric: leadsHandler.metricErrors(),
  threshold: 10,
  evaluationPeriods: 1,
  alarmDescription: "Lambda errors exceeded threshold",
});
Enter fullscreen mode Exit fullscreen mode

Cost Analysis

Let's break down the total monthly cost:

Service Usage Free Tier Cost
S3 100MB, 50K requests 5GB, 20K GET $0.02
CloudFront 10GB, 100K requests 1TB, 10M $0.00
API Gateway 50K requests 1M $0.00
Lambda 50K invocations, 5s average 1M, 400K GB-s $0.00
DynamoDB 1GB, 50K writes 25GB, 25 WCU $0.00
CloudWatch Basic metrics Always free $0.00
Total ~$0.02/month

For a high-traffic site (1M monthly visitors): ~$10-20/month

Compare to traditional hosting: $50-200/month minimum


Next Steps

Now that you've mastered serverless web development, consider:

1. Add Email Notifications

Use SES to send confirmation emails when leads are created.

2. Implement Authentication

Add Cognito for admin dashboard access.

3. Add File Uploads

Accept resume/document uploads stored in S3.

4. Build an Admin Dashboard

Create a separate protected area to manage leads.

5. Add Analytics

Integrate CloudWatch Logs Insights or third-party analytics.


Conclusion

🎉 Congratulations! You've completed the Serverless Web Mastery series!

You've built a complete, production-ready serverless application:

Part 1: S3 + CloudFront for static hosting
Part 2: API Gateway + Lambda for serverless API
Part 3: DynamoDB for persistent storage
Part 4: Full integration with production patterns

What you've learned:

  • Infrastructure as Code with AWS CDK
  • Modern frontend development
  • REST API design
  • NoSQL database patterns
  • Cost optimization techniques
  • Production deployment best practices

The best part? This entire stack follows the serverless-first approach:

  • Zero servers to manage
  • Automatic scaling
  • Pay-per-use pricing
  • Built-in high availability

Related Posts:

GitHub Repository: aws-serverless-website-tutorial

Thank you for joining me on this serverless journey. Now go build something amazing!

Happy coding! 🚀


References

Top comments (0)