If you would like to deploy a React App to AWS S3 and AWS CloudFront, then you can follow this guide.

The following solution creates a React App and deploys it to S3 and CloudFront using the client’s CLI.
It also chains commands so that a React build, S3 sync and CloudFront invalidation can occur with a single command.

Code available at GitHub

https://github.com/ao/deploy-react-to-s3-cloudfront

Target Architecture

Guided Deployment Solution

Create a directory for the application:

mkdir deploy_react && cd $_

Create the React App using create-react-app from npx:

npx create-react-app sample-react-app

(Optional) Open the project in VS Code:

code .

Change directory and run the app:

cd sample-react-app<br>npm start
```<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-4-484x350.png" alt="" class="wp-image-9377" width="820" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-4-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-4-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-4-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-4-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-4.png 1644w" sizes="(max-width: 820px) 100vw, 820px" /> </figure> 

# Install Router

Now we need to install&nbsp;`react-router-dom`&nbsp;so that we can change routes between pages in our React app.

npm i react-router-dom


Once this is done, we can edit our code before moving onto the deployment steps.

# Swap out the code

Open the&nbsp;`App.js`&nbsp;file under the&nbsp;`src`&nbsp;directory and replace all the code in the file with the following:

import ‘./App.css’; import React from “react”; import { BrowserRouter as Router, Routes, Route, Link } from “react-router-dom”;

const Home = () => { return

Home

} const About = () => { return

About

}

function App() { return (

  <Router>
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>

      <div className="content">
        <Routes>
          <Route path="/about" element={<About />} />
          <Route path="/" element={<Home />} />
        </Routes>
      </div>

    </div>
  </Router>

</div>

); }

export default App;


Open the&nbsp;`App.css`&nbsp;file as replace it with the following:

ul { padding: 0; } li { display:inline; padding: 10px; } .content { padding: 0 10px; }


If we run the React app with `npm start`, we will now see the following:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-9-484x350.png" alt="" class="wp-image-9382" width="819" height="592" srcset="https://ao.ms/wp-content/uploads/2022/08/image-9-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-9-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-9-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-9-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-9.png 1644w" sizes="(max-width: 819px) 100vw, 819px" /> </figure> 

If we click on `About` in the navigation, the page changes and shows the `About` component.<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-10-484x350.png" alt="" class="wp-image-9383" width="818" height="592" srcset="https://ao.ms/wp-content/uploads/2022/08/image-10-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-10-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-10-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-10-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-10.png 1644w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

# Setting up S3 and CloudFront in the AWS Management Console

Head over to the S3 console and `create a new bucket`.  
Give it a unique `bucket name` and click `Create bucket.`<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-28-483x350.png" alt="" class="wp-image-9412" width="819" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-28-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-28-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-28-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-28.png 1536w" sizes="(max-width: 819px) 100vw, 819px" /> </figure> 

We now have a new bucket, with nothing inside.<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-29-483x350.png" alt="" class="wp-image-9416" width="824" height="597" srcset="https://ao.ms/wp-content/uploads/2022/08/image-29-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-29-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-29-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-29.png 1536w" sizes="(max-width: 824px) 100vw, 824px" /> </figure> 

Head over to CloudFront and&nbsp;`create a distribution`:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-30-483x350.png" alt="" class="wp-image-9418" width="819" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-30-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-30-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-30-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-30.png 1536w" sizes="(max-width: 819px) 100vw, 819px" /> </figure> 

Select the&nbsp;`Origin domain`, which will be the newly created S3 bucket.  
Specify a&nbsp;`Name`. Note that it will create one for you from the&nbsp;`Origin domain`&nbsp;by default if you don’t specify one yourself.

For S3 bucket access, Choose&nbsp;`Yes use OAI`, create a new OAI and select&nbsp;`Yes`&nbsp;for the&nbsp;`Bucket policy Update`.<figure class="wp-block-image size-full">

<img decoding="async" loading="lazy" width="774" height="340" src="https://ao.ms/wp-content/uploads/2022/08/image-14.png" alt="" class="wp-image-9387" srcset="https://ao.ms/wp-content/uploads/2022/08/image-14.png 774w, https://ao.ms/wp-content/uploads/2022/08/image-14-300x132.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-14-768x337.png 768w" sizes="(max-width: 774px) 100vw, 774px" /> </figure> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="774" height="340" src="https://ao.ms/wp-content/uploads/2022/08/image-14.png" alt="" class="wp-image-9388" srcset="https://ao.ms/wp-content/uploads/2022/08/image-14.png 774w, https://ao.ms/wp-content/uploads/2022/08/image-14-300x132.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-14-768x337.png 768w" sizes="(max-width: 774px) 100vw, 774px" /></figure> 

Under&nbsp;`Default cache behavior`, select&nbsp;`Redirect HTTP to HTTPS.`

Under&nbsp;`Settings`, specify the&nbsp;`Default root object`&nbsp;to be&nbsp;`index.html`

Leave all other fields as is and click `Create distribution`.<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-31-483x350.png" alt="" class="wp-image-9420" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-31-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-31-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-31-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-31.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

You will now see a distribution being created for you.<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-39-483x350.png" alt="" class="wp-image-9432" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-39-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-39-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-39-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-39.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

Note that this will take a couple of minutes to get ready,

# Setting up the Deployment Scripts

In the&nbsp;`package.json`&nbsp;file, under&nbsp;`src/`, locate the following&nbsp;`scripts`&nbsp;lines:

“scripts”: { “start”: “react-scripts start”, “build”: “react-scripts build”, “test”: “react-scripts test”, “eject”: “react-scripts eject” },


Here we will add some more options:  
We will add a new script called&nbsp;`deploy-to-s3`&nbsp;and it will run the following command:  
`aws s3 sync build/ s3://<your_s3_bucket_name>`

Note that you can also specify an AWS_PROFILE here as follows if needed:  
`aws s3 sync build/ s3://<your_s3_bucket_name> --profile <profile_name>`

Update the&nbsp;`scripts`&nbsp;section to look as below, but change your own S3 bucket name inplace:

“scripts”: { “start”: “react-scripts start”, “build”: “react-scripts build”, “deploy-to-s3”: “aws s3 sync build/ s3://sample-react-app-123654789”, “test”: “react-scripts test”, “eject”: “react-scripts eject” },


Now we need to create a&nbsp;`build`&nbsp;of our React app, so that we can push it’s contents to S3.  
To do this, run the following command:  
`npm run build`

Then deploy it to S3 as follows:  
`npm run deploy-to-s3`<figure class="wp-block-image size-large">

<img decoding="async" loading="lazy" width="800" height="253" src="https://ao.ms/wp-content/uploads/2022/08/image-17-800x253.png" alt="" class="wp-image-9391" srcset="https://ao.ms/wp-content/uploads/2022/08/image-17-800x253.png 800w, https://ao.ms/wp-content/uploads/2022/08/image-17-300x95.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-17-768x243.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-17.png 1042w" sizes="(max-width: 800px) 100vw, 800px" /> </figure> 

Now if we look in the S3 console, we can see the files that were deloyed:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-33-483x350.png" alt="" class="wp-image-9423" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-33-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-33-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-33-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-33.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

# Setting up CloudFront pages

We now need to setup the CloudFront pages, which we will do through the CloudFront console.<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-34-483x350.png" alt="" class="wp-image-9424" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-34-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-34-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-34-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-34.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

Under the CloudFront distribution, click `Create custom error response.`  
We do this because React is a Single Page Application (SPA) and no physical files exist on the server for the different `Routes` that we have specified. They are all dynamic.  
For example, `/about` does not exist as a logical path on the drive, or server. So instead, it will be a `404 Not Found`when called upon. So therefore, we will tell CloudFront that for all `404 Not Found` paths, we want `index.html` to handle them.  
Remember that `index.html` is the path for where React initializes.

To this end, create a&nbsp;`404 Not Found`&nbsp;custom error response, that points to our&nbsp;`/index.html`&nbsp;file, with a status of&nbsp;`200 OK`:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-35-483x350.png" alt="" class="wp-image-9425" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-35-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-35-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-35-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-35.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

Also create a&nbsp;`403 Forbidden`&nbsp;custom error response, that points to our&nbsp;`/index.html`&nbsp;file, with a status of&nbsp;`200 OK:`<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-36-483x350.png" alt="" class="wp-image-9426" width="817" height="592" srcset="https://ao.ms/wp-content/uploads/2022/08/image-36-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-36-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-36-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-36.png 1536w" sizes="(max-width: 817px) 100vw, 817px" /> </figure> 

Once both have been created, the `Error pages` should have two (2) entries as follows:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-37-483x350.png" alt="" class="wp-image-9427" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-37-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-37-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-37-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-37.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

If we don’t create these, then we will get the&nbsp;`AccessDenied`&nbsp;error when trying to access any of the&nbsp;`Routes`&nbsp;we specified in the React app, which look like this:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-23-484x350.png" alt="" class="wp-image-9397" width="817" height="591" srcset="https://ao.ms/wp-content/uploads/2022/08/image-23-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-23-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-23-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-23-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-23.png 1644w" sizes="(max-width: 817px) 100vw, 817px" /> </figure> 

Now instead, we can see the actual&nbsp;`Route`&nbsp;itself:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-24-484x350.png" alt="" class="wp-image-9398" width="818" height="592" srcset="https://ao.ms/wp-content/uploads/2022/08/image-24-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-24-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-24-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-24-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-24.png 1644w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

# Improving the Deployment scripts

Everytime we update the CloudFront distribution, by deploying new files to S3, we need to&nbsp;`Invalidate`&nbsp;the files.

Head over to the&nbsp;`package.json`&nbsp;file from before and add another command under the one we just added:  
It will look something like this:

aws cloudfront create-invalidation –distribution-id <distribution_id> –paths ‘/*’ –profile <profile_name>


You don’t need to specify the&nbsp;`--profile`&nbsp;argument, unless you need to.

We can get the Distribution ID from CloudFront itself:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-40-483x350.png" alt="" class="wp-image-9434" width="818" height="593" srcset="https://ao.ms/wp-content/uploads/2022/08/image-40-483x350.png 483w, https://ao.ms/wp-content/uploads/2022/08/image-40-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-40-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-40.png 1536w" sizes="(max-width: 818px) 100vw, 818px" /> </figure> 

Update this new section as follows, remember to replace your&nbsp;`--distribution-id`:

“scripts”: { “start”: “react-scripts start”, “build”: “react-scripts build”, “deploy-to-s3”: “aws s3 sync build/ s3://sample-react-app-123654789”, “invalidate-cloudfront”: “aws cloudfront create-invalidation –distribution-id EIAUK8JFBCT6S – paths ‘/*’”, “test”: “react-scripts test”, “eject”: “react-scripts eject” },


If you run that step alone, you will get a verification as follows:

{ “Location”: “https://cloudfront.amazonaws.com/2020-05-31/distribution/EIAUK8JFBCT6S/invalidation/I17X51041BLJHR", “Invalidation”: { “Id”: “I17X51041BLJHR”, “Status”: “InProgress”, “CreateTime”: “2022-08-17T18:16:56.890000+00:00”, “InvalidationBatch”: { “Paths”: { “Quantity”: 1, “Items”: [ “/*” ] }, “CallerReference”: “cli-1660760215-662979” } } }


Now that we have both the steps we need, let’s create an aggregate command that will tie everything together, so that we only need to run a single command each time:

We will add the following&nbsp;`script`:

“deploy”: “npm run build && npm run deploy-to-s3 && npm run invalidate-cloudfront”,


So once we have added it to the&nbsp;`scripts`&nbsp;block, it will all look like this:

“scripts”: { “start”: “react-scripts start”, “build”: “react-scripts build”, “deploy-to-s3”: “aws s3 sync build/ s3://sample-react-app-123654789”, “invalidate-cloudfront”: “aws cloudfront create-invalidation –distribution-id EIAUK8JFBCT6S –paths ‘/*’”, “deploy”: “npm run build && npm run deploy-to-s3 && npm run invalidate-cloudfront”, “test”: “react-scripts test”, “eject”: “react-scripts eject” },


This now means we have a single command to `build` our React App, `sync` the files to S3, and `invalidate` the files in CloudFront, as a chained command.

# Testing our Deployment scripts

If we take the current state of the deployed application on CloudFront, it looks like this:<figure class="wp-block-image size-large is-resized">

<img decoding="async" loading="lazy" src="https://ao.ms/wp-content/uploads/2022/08/image-26-484x350.png" alt="" class="wp-image-9400" width="817" height="591" srcset="https://ao.ms/wp-content/uploads/2022/08/image-26-484x350.png 484w, https://ao.ms/wp-content/uploads/2022/08/image-26-300x217.png 300w, https://ao.ms/wp-content/uploads/2022/08/image-26-768x556.png 768w, https://ao.ms/wp-content/uploads/2022/08/image-26-1536x1112.png 1536w, https://ao.ms/wp-content/uploads/2022/08/image-26.png 1644w" sizes="(max-width: 817px) 100vw, 817px" /> </figure> 

If we open the&nbsp;`App.js`&nbsp;file and create a new&nbsp;`Route`:

<Route path="/testing” element={} />


Which is added as follows:
} /> } /> } />
```

Then add a new component for Testing:

const Testing = () => {
    return <h2>Testing</h2>
}

Then add a new nav item:

<li>
    <Link to="/testing">Testing</Link>
</li>

Now all we need to do to see the changes deployed, is run the following command:

npm run deploy
```

This will cycle through our steps and produce the following output:

```
> [email protected] deploy
> npm run build && npm run deploy-to-s3 && npm run invalidate-cloudfront


> [email protected] build
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  50.75 kB  build/static/js/main.95dbd789.js
  1.79 kB   build/static/js/787.7c33f095.chunk.js
  301 B     build/static/css/main.58e1094f.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment


> [email protected] deploy-to-s3
> aws s3 sync build/ s3://sample-react-app-123654789

upload: build/asset-manifest.json to s3://sample-react-app-123654789/asset-manifest.json
upload: build/static/js/787.7c33f095.chunk.js.map to s3://sample-react-app-123654789/static/js/787.7c33f095.chunk.js.map
upload: build/index.html to s3://sample-react-app-123654789/index.html
upload: build/robots.txt to s3://sample-react-app-123654789/robots.txt
upload: build/manifest.json to s3://sample-react-app-123654789/manifest.json
upload: build/static/js/787.7c33f095.chunk.js to s3://sample-react-app-123654789/static/js/787.7c33f095.chunk.js
upload: build/favicon.ico to s3://sample-react-app-123654789/favicon.ico
upload: build/static/css/main.58e1094f.css.map to s3://sample-react-app-123654789/static/css/main.58e1094f.css.map
upload: build/static/css/main.58e1094f.css to s3://sample-react-app-123654789/static/css/main.58e1094f.css
upload: build/logo512.png to s3://sample-react-app-123654789/logo512.png
upload: build/logo192.png to s3://sample-react-app-123654789/logo192.png
upload: build/static/js/main.95dbd789.js.LICENSE.txt to s3://sample-react-app-123654789/static/js/main.95dbd789.js.LICENSE.txt
upload: build/static/js/main.95dbd789.js to s3://sample-react-app-123654789/static/js/main.95dbd789.js
upload: build/static/js/main.95dbd789.js.map to s3://sample-react-app-123654789/static/js/main.95dbd789.js.map

> [email protected] invalidate-cloudfront
> aws cloudfront create-invalidation --distribution-id EIAUK8JFBCT6S --paths '/*'
```

Now we can refresh the browser and we will see our new `Route` added and linked to our new `TestingComponent` as soon as the CloudFront invalidations have completed.