The Challenge: Hosting Single-Page React Apps
Deploying a modern JavaScript single-page application (SPA), like one built with React and Vite, to a production-grade, secure, and performant setup requires more than just uploading static files. SPAs typically rely on client-side routing, meaning requests to deep links (e.g., yourdomain.com/users/123) need to be handled by the application's router, not by the web server. A standard static file server would return a 404 for such requests. Furthermore, direct public access to the S3 bucket storing your application's build artifacts is a security risk and bypasses the benefits of a Content Delivery Network (CDN).
This article outlines a robust architecture using AWS S3 for storage and AWS CloudFront as a CDN to serve your React application. This setup ensures your app is accessible globally, delivered over HTTPS, caches efficiently, and correctly handles client-side routing, all while keeping your S3 bucket private.
Architecture Overview: S3 + CloudFront for SPAs
The core of this deployment strategy involves two key AWS services: Amazon S3 and Amazon CloudFront. Your React application, after being built for production (typically with a command like npm run build or yarn build), generates a set of static HTML, CSS, JavaScript, and asset files. These files are uploaded to an S3 bucket. However, this bucket is configured to be private, meaning it's not directly accessible from the internet.
Instead, AWS CloudFront, a global CDN, acts as the front-facing layer. CloudFront is configured to use the S3 bucket as its origin. To allow CloudFront to access the private S3 bucket, a mechanism called Origin Access Control (OAC) is employed. OAC is the successor to Origin Access Identity (OAI) and provides a more secure and flexible way for CloudFront to interact with S3.
When a user requests your application (e.g., yourdomain.com/about), the request first hits CloudFront. CloudFront checks its cache for the requested asset. If found, it serves the asset directly. If not, CloudFront forwards the request to the S3 origin. Thanks to OAC, CloudFront can securely retrieve the file from the private S3 bucket. For client-side routing, CloudFront is configured to return a custom error response for specific HTTP error codes (like 403 Forbidden or 404 Not Found) that indicate a missing file. This custom response points to the application's main HTML file (e.g., index.html), allowing the React router to take over and render the correct view.
Setting up the S3 Bucket
The first step is to create an S3 bucket. It's crucial to name it appropriately, often matching your domain name (e.g., your-react-app-bucket). Critically, this bucket must NOT be configured for public access. Block all public access settings should be enabled. This is the primary security measure to ensure your build artifacts are not directly exposed.
Once the bucket is created, you need to upload your React application's build output. This typically involves running your build command (e.g., npm run build) and then using the AWS CLI or the AWS Management Console to sync the contents of your local build directory (e.g., build/ or dist/) to the S3 bucket.

Configuring CloudFront Distribution
With the S3 bucket prepared, the next major step is to set up an AWS CloudFront distribution. When creating a new distribution, you'll need to specify the S3 bucket as the origin. Here's where OAC comes into play:
- Origin Domain: Select your S3 bucket from the dropdown.
- Origin Access: Choose 'Origin access control settings (recommended)'. Create a new OAC setting or select an existing one.
- Bucket Policy: CloudFront will provide a bucket policy that grants the CloudFront distribution the necessary permissions to read objects from your S3 bucket. You must manually add this policy to your S3 bucket's permissions. This policy typically uses a service principal for CloudFront and a condition that restricts access to the specific distribution.
Beyond the origin configuration, several other CloudFront settings are vital for SPAs:
- Viewer Protocol Policy: Set this to 'Redirect HTTP to HTTPS' to enforce secure connections.
- Allowed HTTP Methods: Ensure 'GET, HEAD' are allowed. If your app makes PUT/POST requests to an API, those are handled separately and don't affect static asset serving.
- Cache Policy: Use a managed policy like 'CachingOptimized' or create a custom one. For SPAs, it's often beneficial to cache static assets aggressively but ensure that cache invalidation is handled properly when deploying new versions.
- Error Pages: This is critical for client-side routing. Configure custom error responses. For 'HTTP Error Code' 403 (Forbidden) and 404 (Not Found), set the 'Response Page Path' to your application's entry point (e.g.,
/index.html) and the 'HTTP Response Code' to 200 (OK). This tells CloudFront that when it can't find a specific file, it should serveindex.htmlinstead, letting your React router handle the URL.
After creating the CloudFront distribution, it will take some time for the changes to propagate globally across all edge locations. You will receive a CloudFront domain name (e.g., d123abc.cloudfront.net) which you can use to test your deployment.
Integrating with a Custom Domain (Optional but Recommended)
To use your own domain name (e.g., www.yourdomain.com) instead of the CloudFront-provided domain, you'll need to configure DNS records. This involves:
- AWS Certificate Manager (ACM): Request or import an SSL/TLS certificate for your custom domain. This certificate must be in the
us-east-1region for CloudFront. - CloudFront Alternate Domain Names (CNAMEs): In your CloudFront distribution settings, add your custom domain name(s) (e.g.,
www.yourdomain.com,yourdomain.com) to the 'Alternate domain names (CNAMEs)' field. - CloudFront SSL Certificate: Select the ACM certificate you created for your domain.
- DNS Provider: Update your domain's DNS records (e.g., with Route 53, GoDaddy, etc.) to point your custom domain to the CloudFront distribution domain name. Typically, this is done using an 'A' record with the 'Alias' option enabled if your provider supports it (like Route 53) or a CNAME record pointing to the CloudFront domain.
Once DNS propagation is complete, your React application will be accessible via your custom domain, served securely over HTTPS by CloudFront.
Deployment Workflow
A streamlined deployment process is key for managing updates. The general workflow involves:
- Build the React App: Run your build command (e.g.,
npm run build) to generate optimized static assets. - Invalidate CloudFront Cache: Before uploading new assets, it's good practice to invalidate the CloudFront cache for the entire distribution or specific paths. This ensures users receive the latest version of your application immediately after deployment. You can do this via the AWS CLI (
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*") or the AWS Console. - Upload to S3: Use the AWS CLI to sync your local build directory to the S3 bucket (e.g.,
aws s3 sync build/ s3://your-react-app-bucket/ --delete). The--deleteflag removes any files from the bucket that are no longer present in your local build, preventing stale assets.
Automating this workflow using CI/CD pipelines (e.g., GitHub Actions, AWS CodePipeline) is highly recommended for efficient and repeatable deployments.
Key Takeaways and Best Practices
Deploying a React app with S3 and CloudFront provides a scalable, secure, and cost-effective solution for hosting static websites and SPAs. Key considerations include:
- Security First: Always keep your S3 bucket private and use OAC for CloudFront access.
- Client-Side Routing: Configure CloudFront error pages correctly to serve
index.htmlfor 403/404 errors. - Caching Strategy: Understand CloudFront caching and implement cache invalidation for deployments. Set appropriate cache-control headers on your S3 objects if needed.
- Custom Domain & HTTPS: Use ACM for SSL/TLS certificates and configure CNAMEs for your custom domain.
- Automation: Automate your build, invalidation, and upload process with CI/CD tools.
This setup is not just for React; it's a highly effective pattern for deploying any modern JavaScript framework (Vue, Angular, Svelte) or even static HTML sites that require global distribution, low latency, and secure hosting.
