Deploy React SPA to AWS S3 + CloudFront (Step-by-Step)

ReactJS, AWS, DevOps|SEPTEMBER 17, 2025|0 VIEWS
Complete guide to deploying React Single Page Applications to AWS S3 with CloudFront CDN for optimal performance and global distribution

Introduction

Deploying a React Single Page Application (SPA) to production requires careful consideration of hosting, performance, and scalability. AWS S3 combined with CloudFront provides one of the most cost-effective and performant solutions for hosting static React applications.

This comprehensive guide will walk you through the complete process of deploying your React SPA to AWS S3 with CloudFront CDN, including proper routing configuration for SPAs, SSL certificate setup, and performance optimization. By the end of this tutorial, you'll have a production-ready deployment pipeline that scales globally.

Why AWS S3 + CloudFront for React SPAs?

Benefits of This Architecture

The combination of AWS S3 and CloudFront offers several advantages for React applications:

// Architecture Benefits
const AwsArchitectureBenefits = {
  cost: "Pay only for storage and data transfer used",
  performance: "Global CDN with edge locations worldwide",
  scalability: "Automatic scaling to handle traffic spikes",
  security: "SSL/TLS encryption and AWS security features",
  reliability: "99.99% uptime SLA with multiple availability zones",
  simplicity: "No server management required",
};

// Cost comparison example
const CostComparison = {
  traditionalHosting: "$20-100/month for VPS",
  awsS3CloudFront: "$1-10/month for most applications",
  savings: "Up to 90% cost reduction for static sites",
};

When to Choose This Solution

This deployment method is ideal for:

  • React SPAs with client-side routing
  • Static sites generated with Create React App, Vite, or similar tools
  • Global applications requiring fast load times worldwide
  • Cost-conscious projects with variable traffic patterns
  • Scalable solutions that need to handle traffic spikes

Prerequisites

Before we begin, ensure you have:

# Required tools and accounts
✅ AWS Account with billing enabled
✅ AWS CLI installed and configured
✅ Node.js and npm/yarn installed
✅ React application ready for production
✅ Custom domain (optional but recommended)

Setting Up AWS CLI

If you haven't set up AWS CLI yet:

# Install AWS CLI (Windows)
winget install Amazon.AWSCLI

# Configure AWS CLI
aws configure
# AWS Access Key ID: [Your access key]
# AWS Secret Access Key: [Your secret key]
# Default region name: us-east-1
# Default output format: json

Step 1: Prepare Your React Application

Build for Production

First, ensure your React app is optimized for production:

# Navigate to your React project
cd your-react-app

# Install dependencies
npm install

# Create production build
npm run build

Optimize Build Configuration

Configure Public URL

For S3 deployment, update your package.json:

{
  "name": "your-react-app",
  "version": "1.0.0",
  "homepage": "https://your-domain.com",
  "scripts": {
    "build": "react-scripts build",
    "deploy": "npm run build && aws s3 sync build/ s3://your-bucket-name --delete"
  }
}

Environment Variables

Create a .env.production file for production-specific configurations:

# .env.production
REACT_APP_API_URL=https://api.your-domain.com
REACT_APP_ENV=production
GENERATE_SOURCEMAP=false

Build Analysis

Analyze your build to ensure optimal bundle size:

# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer

# Analyze build
npx webpack-bundle-analyzer build/static/js/*.js

Step 2: Create and Configure S3 Bucket

Create S3 Bucket

# Create S3 bucket (bucket names must be globally unique)
aws s3 mb s3://your-unique-bucket-name --region us-east-1

# Example with timestamp
aws s3 mb s3://my-react-app-$(date +%s) --region us-east-1

Configure Bucket for Static Website Hosting

# Enable static website hosting
aws s3 website s3://your-bucket-name \
  --index-document index.html \
  --error-document error.html

Set Bucket Policy for Public Access

Create a bucket policy file bucket-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

Apply the policy:

# Apply bucket policy
aws s3api put-bucket-policy \
  --bucket your-bucket-name \
  --policy file://bucket-policy.json

# Disable block public access
aws s3api put-public-access-block \
  --bucket your-bucket-name \
  --public-access-block-configuration \
  BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false

Step 3: Deploy React App to S3

Upload Build Files

# Sync build folder to S3
aws s3 sync build/ s3://your-bucket-name --delete

# With specific cache settings
aws s3 sync build/ s3://your-bucket-name \
  --delete \
  --cache-control "public, max-age=31536000" \
  --exclude "*.html" \
  --exclude "service-worker.js"

# HTML files with shorter cache
aws s3 sync build/ s3://your-bucket-name \
  --cache-control "public, max-age=0, must-revalidate" \
  --include "*.html" \
  --include "service-worker.js"

Automated Deployment Script

Create a deployment script deploy.sh:

#!/bin/bash
set -e

BUCKET_NAME="your-bucket-name"
DISTRIBUTION_ID="your-cloudfront-distribution-id"

echo "🏗️  Building React app..."
npm run build

echo "📤 Uploading to S3..."
# Upload with long cache for static assets
aws s3 sync build/ s3://$BUCKET_NAME \
  --delete \
  --cache-control "public, max-age=31536000" \
  --exclude "*.html" \
  --exclude "service-worker.js" \
  --exclude "asset-manifest.json"

# Upload HTML files with no cache
aws s3 sync build/ s3://$BUCKET_NAME \
  --cache-control "public, max-age=0, must-revalidate" \
  --include "*.html" \
  --include "service-worker.js" \
  --include "asset-manifest.json"

echo "🔄 Invalidating CloudFront..."
aws cloudfront create-invalidation \
  --distribution-id $DISTRIBUTION_ID \
  --paths "/*"

echo "✅ Deployment complete!"

Step 4: Set Up CloudFront Distribution

Create CloudFront Distribution

# Create distribution configuration
cat > cloudfront-config.json << EOF
{
  "CallerReference": "$(date +%s)",
  "DefaultRootObject": "index.html",
  "Origins": {
    "Quantity": 1,
    "Items": [
      {
        "Id": "S3Origin",
        "DomainName": "your-bucket-name.s3.us-east-1.amazonaws.com",
        "S3OriginConfig": {
          "OriginAccessIdentity": ""
        }
      }
    ]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "S3Origin",
    "ViewerProtocolPolicy": "redirect-to-https",
    "MinTTL": 0,
    "DefaultTTL": 86400,
    "MaxTTL": 31536000,
    "ForwardedValues": {
      "QueryString": false,
      "Cookies": {
        "Forward": "none"
      }
    },
    "TrustedSigners": {
      "Enabled": false,
      "Quantity": 0
    }
  },
  "Comment": "React SPA Distribution",
  "Enabled": true,
  "PriceClass": "PriceClass_All"
}
EOF

# Create distribution
aws cloudfront create-distribution \
  --distribution-config file://cloudfront-config.json

Configure SPA Routing

For React Router to work properly, configure custom error pages:

# Update distribution for SPA routing
aws cloudfront update-distribution \
  --id YOUR_DISTRIBUTION_ID \
  --distribution-config '{
    "CallerReference": "spa-routing-update",
    "CustomErrorResponses": {
      "Quantity": 2,
      "Items": [
        {
          "ErrorCode": 404,
          "ResponsePagePath": "/index.html",
          "ResponseCode": "200",
          "ErrorCachingMinTTL": 300
        },
        {
          "ErrorCode": 403,
          "ResponsePagePath": "/index.html",
          "ResponseCode": "200",
          "ErrorCachingMinTTL": 300
        }
      ]
    }
  }'

Optimize Cache Behaviors

Create optimized cache behaviors for different file types:

{
  "CacheBehaviors": {
    "Quantity": 3,
    "Items": [
      {
        "PathPattern": "*.js",
        "TargetOriginId": "S3Origin",
        "ViewerProtocolPolicy": "https-only",
        "DefaultTTL": 31536000,
        "Compress": true
      },
      {
        "PathPattern": "*.css",
        "TargetOriginId": "S3Origin",
        "ViewerProtocolPolicy": "https-only",
        "DefaultTTL": 31536000,
        "Compress": true
      },
      {
        "PathPattern": "*.html",
        "TargetOriginId": "S3Origin",
        "ViewerProtocolPolicy": "https-only",
        "DefaultTTL": 0,
        "Compress": true
      }
    ]
  }
}

Step 5: Configure Custom Domain and SSL

Request SSL Certificate

# Request certificate in us-east-1 (required for CloudFront)
aws acm request-certificate \
  --domain-name your-domain.com \
  --subject-alternative-names www.your-domain.com \
  --validation-method DNS \
  --region us-east-1

Validate Certificate

# Get certificate validation records
aws acm describe-certificate \
  --certificate-arn your-certificate-arn \
  --region us-east-1

Add the DNS validation records to your domain's DNS settings.

Update CloudFront with Custom Domain

# Update distribution with custom domain
aws cloudfront update-distribution \
  --id YOUR_DISTRIBUTION_ID \
  --distribution-config '{
    "Aliases": {
      "Quantity": 2,
      "Items": ["your-domain.com", "www.your-domain.com"]
    },
    "ViewerCertificate": {
      "ACMCertificateArn": "your-certificate-arn",
      "SSLSupportMethod": "sni-only",
      "MinimumProtocolVersion": "TLSv1.2_2021"
    }
  }'

Update DNS Records

Point your domain to CloudFront:

# Create ALIAS record for apex domain
# your-domain.com -> d1234567890.cloudfront.net

# Create CNAME record for www subdomain
# www.your-domain.com -> d1234567890.cloudfront.net

Step 6: Implement CI/CD Pipeline

GitHub Actions Deployment

Create .github/workflows/deploy.yml:

name: Deploy to AWS S3 + CloudFront

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          REACT_APP_API_URL: ${{ secrets.REACT_APP_API_URL }}

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Deploy to S3
        run: |
          aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} \
            --delete \
            --cache-control "public, max-age=31536000" \
            --exclude "*.html" \
            --exclude "service-worker.js"

          aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} \
            --cache-control "public, max-age=0, must-revalidate" \
            --include "*.html" \
            --include "service-worker.js"

      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/*"

Environment Variables Setup

Add these secrets to your GitHub repository:

# GitHub repository settings > Secrets and variables > Actions
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
S3_BUCKET=your-bucket-name
CLOUDFRONT_DISTRIBUTION_ID=your-distribution-id
REACT_APP_API_URL=https://api.your-domain.com

Step 7: Performance Optimization

Enable Compression

Ensure CloudFront compression is enabled:

{
  "DefaultCacheBehavior": {
    "Compress": true,
    "ViewerProtocolPolicy": "redirect-to-https"
  }
}

Optimize React Build

Code Splitting

Implement route-based code splitting:

// App.js
import { lazy, Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

// Lazy load components
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Contact = lazy(() => import("./pages/Contact"));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

export default App;

Bundle Analysis and Optimization

// webpack-bundle-analyzer setup
const BundleAnalyzerPlugin =
  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      openAnalyzer: false,
      reportFilename: "bundle-report.html",
    }),
  ],
};

CloudFront Optimization

Custom Cache Headers

# Set appropriate cache headers for different file types
aws s3 cp build/ s3://your-bucket/ --recursive \
  --cache-control "public, max-age=31536000, immutable" \
  --include "*.js" --include "*.css" --include "*.woff*" --include "*.png" --include "*.jpg"

aws s3 cp build/ s3://your-bucket/ --recursive \
  --cache-control "public, max-age=0, must-revalidate" \
  --include "*.html" --include "service-worker.js"

HTTP/2 and GZIP

CloudFront automatically enables:

  • HTTP/2 for improved multiplexing
  • GZIP compression for text-based assets
  • Brotli compression for better compression ratios

Step 8: Monitoring and Analytics

CloudWatch Metrics

Monitor your deployment with CloudWatch:

# Get CloudFront metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/CloudFront \
  --metric-name Requests \
  --dimensions Name=DistributionId,Value=YOUR_DISTRIBUTION_ID \
  --start-time 2023-01-01T00:00:00Z \
  --end-time 2023-01-02T00:00:00Z \
  --period 3600 \
  --statistics Sum

Real User Monitoring

Implement performance monitoring in your React app:

// Performance monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";

function sendToAnalytics(metric) {
  // Send to your analytics service
  console.log(metric);
}

// Measure Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Error Tracking

Set up error boundaries and tracking:

// ErrorBoundary.js
import React from "react";

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // Log error to monitoring service
    console.error("Error caught by boundary:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>
            We apologize for the inconvenience. Please try refreshing the page.
          </p>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Troubleshooting Common Issues

SPA Routing Problems

Problem: 404 errors on direct URL access or page refresh.

Solution: Ensure CloudFront custom error responses are configured:

{
  "CustomErrorResponses": {
    "Items": [
      {
        "ErrorCode": 404,
        "ResponsePagePath": "/index.html",
        "ResponseCode": "200"
      },
      {
        "ErrorCode": 403,
        "ResponsePagePath": "/index.html",
        "ResponseCode": "200"
      }
    ]
  }
}

Cache Issues

Problem: Updates not reflected immediately.

Solution: Create CloudFront invalidation:

# Invalidate all files
aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/*"

# Invalidate specific files
aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/index.html" "/static/js/*"

CORS Issues

Problem: API calls failing due to CORS.

Solution: Configure CORS in your S3 bucket:

{
  "CORSRules": [
    {
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["GET", "POST", "PUT", "DELETE"],
      "AllowedOrigins": ["https://your-domain.com"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3000
    }
  ]
}

SSL Certificate Issues

Problem: Certificate not validating or not applying.

Solution: Ensure certificate is in us-east-1 region and properly validated:

# Check certificate status
aws acm describe-certificate \
  --certificate-arn your-certificate-arn \
  --region us-east-1

# List certificates in us-east-1
aws acm list-certificates --region us-east-1

Cost Optimization

S3 Storage Classes

Optimize costs with appropriate storage classes:

# Configure lifecycle policy for old versions
aws s3api put-bucket-lifecycle-configuration \
  --bucket your-bucket-name \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "DeleteOldVersions",
      "Status": "Enabled",
      "Filter": {"Prefix": ""},
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      }
    }]
  }'

CloudFront Pricing

Choose appropriate price class:

{
  "PriceClass": "PriceClass_100", // Use only US, Canada, Europe
  "PriceClass": "PriceClass_200", // Add Asia, Australia
  "PriceClass": "PriceClass_All" // Global distribution (highest cost)
}

Monitoring Costs

Set up billing alerts:

# Create billing alarm
aws cloudwatch put-metric-alarm \
  --alarm-name "AWS-Billing-Alert" \
  --alarm-description "Alert when AWS bill exceeds $10" \
  --metric-name EstimatedCharges \
  --namespace AWS/Billing \
  --statistic Maximum \
  --period 86400 \
  --threshold 10 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=Currency,Value=USD

Advanced Configuration

Multi-Environment Setup

Manage multiple environments (dev, staging, prod):

# Environment-specific deployment
#!/bin/bash
ENVIRONMENT=${1:-dev}
BUCKET_NAME="myapp-${ENVIRONMENT}"
DISTRIBUTION_ID=$(aws cloudfront list-distributions \
  --query "DistributionList.Items[?Comment=='${ENVIRONMENT}'].Id" \
  --output text)

echo "Deploying to ${ENVIRONMENT} environment..."

# Build with environment-specific config
REACT_APP_ENV=${ENVIRONMENT} npm run build

# Deploy to environment-specific bucket
aws s3 sync build/ s3://${BUCKET_NAME} --delete

# Invalidate environment-specific distribution
aws cloudfront create-invalidation \
  --distribution-id ${DISTRIBUTION_ID} \
  --paths "/*"

Security Headers

Implement security headers with Lambda@Edge:

// security-headers.js (Lambda@Edge function)
exports.handler = (event, context, callback) => {
  const response = event.Records[0].cf.response;
  const headers = response.headers;

  headers["strict-transport-security"] = [
    {
      key: "Strict-Transport-Security",
      value: "max-age=31536000; includeSubDomains",
    },
  ];

  headers["content-security-policy"] = [
    {
      key: "Content-Security-Policy",
      value: "default-src 'self'; script-src 'self' 'unsafe-inline'",
    },
  ];

  headers["x-content-type-options"] = [
    {
      key: "X-Content-Type-Options",
      value: "nosniff",
    },
  ];

  headers["x-frame-options"] = [
    {
      key: "X-Frame-Options",
      value: "DENY",
    },
  ];

  headers["referrer-policy"] = [
    {
      key: "Referrer-Policy",
      value: "strict-origin-when-cross-origin",
    },
  ];

  callback(null, response);
};

Blue-Green Deployment

Implement zero-downtime deployments:

#!/bin/bash
# Blue-Green deployment script
BLUE_BUCKET="myapp-blue"
GREEN_BUCKET="myapp-green"
CURRENT_ORIGIN=$(aws cloudfront get-distribution \
  --id $DISTRIBUTION_ID \
  --query 'Distribution.DistributionConfig.Origins.Items[0].DomainName' \
  --output text)

if [[ $CURRENT_ORIGIN == *"blue"* ]]; then
    TARGET_BUCKET=$GREEN_BUCKET
    echo "Current: Blue, Deploying to: Green"
else
    TARGET_BUCKET=$BLUE_BUCKET
    echo "Current: Green, Deploying to: Blue"
fi

# Deploy to target bucket
aws s3 sync build/ s3://$TARGET_BUCKET --delete

# Update CloudFront origin
aws cloudfront update-distribution \
  --id $DISTRIBUTION_ID \
  --distribution-config "$(aws cloudfront get-distribution-config \
    --id $DISTRIBUTION_ID \
    --query 'DistributionConfig' \
    --output json | \
    jq ".Origins.Items[0].DomainName = \"$TARGET_BUCKET.s3.amazonaws.com\"")"

echo "Switched to $TARGET_BUCKET"

Best Practices and Security

Security Checklist

✅ Use HTTPS only (redirect HTTP to HTTPS)
✅ Implement proper CORS policies
✅ Use least privilege IAM policies
✅ Enable CloudTrail logging
✅ Set up proper bucket policies
✅ Use AWS WAF for additional protection
✅ Implement security headers
✅ Regular security audits

Performance Checklist

✅ Enable GZIP/Brotli compression
✅ Implement proper caching strategies
✅ Use code splitting and lazy loading
✅ Optimize images and assets
✅ Minimize bundle sizes
✅ Use HTTP/2 features
✅ Implement service workers for caching
✅ Monitor Core Web Vitals

Deployment Checklist

✅ Test build locally before deployment
✅ Verify all environment variables
✅ Check routing configuration
✅ Test SSL certificate
✅ Validate DNS settings
✅ Monitor deployment metrics
✅ Set up alerts and monitoring
✅ Document rollback procedures

Conclusion

Deploying a React SPA to AWS S3 with CloudFront provides a scalable, cost-effective, and performant hosting solution. This setup offers:

  • Global Performance: CloudFront's edge locations ensure fast load times worldwide
  • Cost Efficiency: Pay only for what you use with S3 and CloudFront pricing
  • Scalability: Automatic scaling to handle any amount of traffic
  • Security: SSL/TLS encryption and AWS security features
  • Reliability: High availability with AWS's infrastructure

By following this comprehensive guide, you now have:

  1. Production-ready deployment process
  2. Automated CI/CD pipeline with GitHub Actions
  3. Performance optimization techniques
  4. Monitoring and troubleshooting strategies
  5. Security best practices implementation

Next Steps

Consider implementing these advanced features:

  • API Gateway integration for backend APIs
  • AWS Amplify for more integrated development workflow
  • Lambda@Edge for advanced request/response processing
  • AWS WAF for additional security
  • Multi-region deployment for disaster recovery

With this foundation, your React application is ready to scale and perform optimally in production. The combination of S3 and CloudFront provides enterprise-grade hosting that can handle everything from small projects to large-scale applications serving millions of users.

Remember to regularly monitor your application's performance, update dependencies, and review your AWS costs to ensure optimal operation over time.