How to Deploy a React App to an AWS S3 Bucket

Introduction

If you’ve read my previous posts on AWS S3, you already know that S3 is great for file storage. But here is a cool tip: since frontend projects (like React, Angular, or Vue) build into static files, we can host them on S3 and use them just like a regular website!

In this post, I’ll show you how to deploy a React app to S3 for direct access, as well as how to set it up using CloudFront.


Prerequisites

Before we start, you should have a basic understanding of AWS S3, how to set up the AWS CDK, and how to create a Bucket.


Implementation

First, let's create a simple React project using Vite with two pages: Home and About. Feel free to use your own content!


App.tsx

import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './page/Home';
import About from './page/About';

function App() {
  return (
    <Router>
      <nav style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
        <Link to="/" style={{ marginRight: '10px' }}>Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <div style={{ padding: '20px' }}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;


Home.tsx

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>Welcome</p>
    </div>
  );
}

export default Home;


About.tsx

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>About page</p>
    </div>
  );
};

export default About;


After coding, run the build command to generate the dist folder. We will use this folder for the CDK deployment.


1. Deploying the React App directly to AWS S3

Create a file named bin/react-app-s3-stack.ts with the following content:

import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import * as path from "path";

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

    const bucket = new s3.Bucket(this, "ReactAppBucket", {
      bucketName: "react-vite-app-1bf8d742",
      websiteIndexDocument: "index.html",
      websiteErrorDocument: "index.html",
      publicReadAccess: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS_ONLY,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    new s3deploy.BucketDeployment(this, "DeployWebsite", {
      sources: [
        s3deploy.Source.asset(
          path.join(__dirname, "../path-to-dist"),
        ),
      ],
      destinationBucket: bucket,
    });

    new cdk.CfnOutput(this, "WebsiteURL", {
      value: bucket.bucketWebsiteUrl,
    });
  }
}

What’s happening here?

  • bucketName: Needs to be globally unique across all of AWS.
  • publicReadAccess: Allows people to view your website files publicly.
  • websiteIndex/ErrorDocument: We set both to index.html. This ensures that if a user refreshes the page on a React route, the app still loads correctly.
  • blockPublicAccess: We modified this to allow direct S3 link access. However, this isn't the most secure method, which is why we’ll look at the CloudFront solution next.


2. Deploying to AWS S3 with CloudFront

For better security and performance, we should use CloudFront. Create bin/react-app-s3-cloudfront-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 s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import * as path from "path";

export class ReactAppS3CloudfrontDeployStack extends cdk.Stack {
  distDirectory = path.resolve(__dirname, "../deploy/react-vite-v19/dist");

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const websiteBucket = new s3.Bucket(this, "ReactAppCloudfrontBucket", {
      bucketName: "react-vite-app-cloudfront-1bf8d742",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    const errorResponses = [403, 404].map((code) => ({
      httpStatus: code,
      responseHttpStatus: 200,
      responsePagePath: "/index.html",
      ttl: cdk.Duration.seconds(0),
    }));

    const distribution = new cloudfront.Distribution(this, "SiteDistribution", {
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(websiteBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        compress: true,
      },
      defaultRootObject: "index.html",
      errorResponses,
      priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
    });

    new s3deploy.BucketDeployment(this, "DeployWebsite", {
      sources: [s3deploy.Source.asset(this.distDirectory)],
      destinationBucket: websiteBucket,
      distribution,
      distributionPaths: ["/*"],
      cacheControl: [
        s3deploy.CacheControl.setPublic(),
        s3deploy.CacheControl.maxAge(cdk.Duration.days(365)),
        s3deploy.CacheControl.sMaxAge(cdk.Duration.days(365)),
      ],
    });

    new s3deploy.BucketDeployment(this, "DeployIndex", {
      sources: [
        s3deploy.Source.asset(this.distDirectory, {
          exclude: ["*", "!index.html"],
        }),
      ],
      destinationBucket: websiteBucket,
      distribution,
      distributionPaths: ["/index.html"],
      cacheControl: [
        s3deploy.CacheControl.setPublic(),
        s3deploy.CacheControl.maxAge(cdk.Duration.minutes(0)),
        s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(0)),
        s3deploy.CacheControl.mustRevalidate(),
      ],
      prune: false,
    });

    new cdk.CfnOutput(this, "URL", { value: distribution.domainName });
  }
}

Key Points:

  • blockPublicAccess: Now set to BLOCK_ALL. Only CloudFront can talk to S3 internally, making it much more secure.
  • errorResponses: This handles "Page Not Found" errors by redirecting them to index.html, allowing React Router to take over.
  • priceClass: Helps manage costs based on location:
    • PRICE_CLASS_100: North America and Europe only (Cheapest).
    • PRICE_CLASS_200: Includes Asia and Australia.
    • PRICE_CLASS_ALL: Everywhere (Most expensive).
  • Why two deployment steps?
    • Step 1 (Assets): We upload everything with a long cache (365 days). Since Vite uses file hashing (e.g., main.123hash.js), these files can be safely cached forever.
    • Step 2 (index.html): We override index.html with no-cache settings. This ensures users always get the latest version of your app. Note that prune: false is vital so we don't delete the assets from Step 1.


Update your bin/aws-cdk.ts file to include both stacks:

#!/usr/bin/env node
import * as cdk from "aws-cdk-lib/core";
import { ReactAppS3DeployStack } from "../lib/react-app-s3-stack";
import { ReactAppS3CloudfrontDeployStack } from "../lib/react-app-s3-cloudfront-stack";

const app = new cdk.App();
new ReactAppS3DeployStack(app, "ReactAppS3DeployStack");
new ReactAppS3CloudfrontDeployStack(app, "ReactAppS3CloudfrontDeployStack");


After a successful deployment, you will see the corresponding S3 and CloudFront URLs in the output.


The direct S3 link uses HTTP and not secure.


The CloudFront link supports HTTPS. While CloudFront provides a random domain name, you can always link it to your own custom domain later.


Your files are now efficiently cached and securely served!



You can check the resources that were created on AWS accordingly.




See you again in the next article!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine