<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Arthur Colman</title>
    <description>The latest articles on Forem by Arthur Colman (@arthurcolman).</description>
    <link>https://forem.com/arthurcolman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3476580%2Fce88f696-7ec9-40c7-b900-e9783a95d696.jpeg</url>
      <title>Forem: Arthur Colman</title>
      <link>https://forem.com/arthurcolman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/arthurcolman"/>
    <language>en</language>
    <item>
      <title>Deploy a React SPA using AWS S3 and CloudFront</title>
      <dc:creator>Arthur Colman</dc:creator>
      <pubDate>Wed, 03 Sep 2025 02:44:19 +0000</pubDate>
      <link>https://forem.com/arthurcolman/deploy-a-react-spa-using-aws-s3-and-cloudfront-8lp</link>
      <guid>https://forem.com/arthurcolman/deploy-a-react-spa-using-aws-s3-and-cloudfront-8lp</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The pattern provided in this post is a step-by-step approach to hosting an SPA that’s written in React on AWS S3 and AWS CloudFront. The SPA in this pattern uses a REST API that’s configured in AWS API Gateway and exposed through an AWS CloudFront distribution to simplify cross-origin resource sharing (CORS) management.&lt;/p&gt;

&lt;p&gt;We're going to be using &lt;a href="https://github.com/aws-samples/react-cors-spa" rel="noopener noreferrer"&gt;this&lt;/a&gt; sample repository provided by AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architeture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevz3nqejv0706izcxaly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevz3nqejv0706izcxaly.png" alt="Architeture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Services
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS API Gateway&lt;/strong&gt; helps you create, publish, maintain, monitor, and secure REST, HTTP, and WebSocket APIs at any scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudFormation&lt;/strong&gt; helps you set up AWS resources, provision them quickly and consistently, and manage them throughout their lifecycle across AWS accounts and Regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudFront&lt;/strong&gt; speeds up distribution of your web content by delivering it through a worldwide network of data centers, which lowers latency and improves performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudTrail&lt;/strong&gt; helps you audit the governance, compliance, and operational risk of your AWS account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudWatch&lt;/strong&gt; helps you monitor the metrics of your AWS resources and the applications you run on AWS in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS IAM&lt;/strong&gt; helps you securely manage access to your AWS resources by controlling who is authenticated and authorized to use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Route 53&lt;/strong&gt; is a highly available and scalable DNS web service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3&lt;/strong&gt; is a cloud-based object storage service that helps you store, protect, and retrieve any amount of data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CloudFormation (IaC)
&lt;/h2&gt;

&lt;p&gt;In the sample repository, we will primarily focus on the information available on the &lt;em&gt;react-cors-spa-stack.yaml&lt;/em&gt; file. In this YAML file, we have all the instructions for AWS CloudFormation to provision the other AWS resources needed in this sample.&lt;/p&gt;

&lt;p&gt;For this post, we won't dive into details of the resources used to provision the REST API of the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS S3 Bucket
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PublicAccessBlockConfiguration&lt;/code&gt; actively blocks all public access&lt;/li&gt;
&lt;li&gt;Bucket logs are enabled and being stored in the &lt;code&gt;LoggingBucket&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BucketName&lt;/code&gt; sets the name of the bucket (that's important information if you are having trouble finding the bucket in the AWS Console)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub 'react-cors-spa-${SimpleAPI}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls : true
        BlockPublicPolicy : true
        IgnorePublicAcls : true
        RestrictPublicBuckets : true
      LoggingConfiguration:
        DestinationBucketName: !Ref LoggingBucket
        LogFilePrefix: s3-access-logs
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: 'AES256'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  AWS S3 BucketPolicy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Allows &lt;code&gt;s3:GetObject*&lt;/code&gt; Action&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CFDistributionSPA&lt;/code&gt; is the only entity allowed to perform the Action &lt;code&gt;s3:GetObject*&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      PolicyDocument:
        Id: MyPolicy
        Version: 2012-10-17
        Statement:
          - Sid: PolicyForCloudFrontPrivateContent
            Effect: Allow
            Resource: !Sub ${S3Bucket.Arn}/*
            Principal:
              Service: cloudfront.amazonaws.com
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CFDistributionSPA}
            Action: 's3:GetObject*'
      Bucket: !Ref S3Bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CloudFront
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sets the origin as the SPA S3 Bucket, using the &lt;code&gt;CloudFrontOriginAccessControl&lt;/code&gt; as the OAC method between CloudFront and AWS S3&lt;/li&gt;
&lt;li&gt;Property &lt;code&gt;DefaultRootObject&lt;/code&gt; receives &lt;code&gt;index.html&lt;/code&gt;. That's the file from the AWS S3 Bucket that CloudFront is going to consider as being the root object.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  CFDistributionSPA:
    #checkov:skip=CKV_AWS_68: "For demo purposes and to reduce cost, no WAF is configured"
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Origins:
        - DomainName: !GetAtt S3Bucket.RegionalDomainName
          Id: myS3Origin
          S3OriginConfig:
            OriginAccessIdentity: ""
          OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
        Enabled: 'true'
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          - OPTIONS
          TargetOriginId: myS3Origin
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
          OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin
          ResponseHeadersPolicyId: eaab4381-ed33-4a86-88ca-d9558dc6cd63 # CORS-with-preflight-and-SecurityHeadersPolicy
          ViewerProtocolPolicy: redirect-to-https
        CustomErrorResponses:
          - ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /index.html
          - ErrorCode: 501
            ResponseCode: 501
            ErrorCachingMinTTL: 0
        PriceClass: PriceClass_All
        Logging:
          Bucket: !GetAtt LoggingBucket.RegionalDomainName
          Prefix: 'cf-spa-access-logs'
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
          MinimumProtocolVersion: 'TLSv1.2_2021'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CloudFront OriginAccessControl
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Describes the request signatures between CloudFront and AWS S3 Bucket (OAC)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    DependsOn:
      - S3Bucket
    Properties: 
      OriginAccessControlConfig:
        Description: Default Origin Access Control
        Name: !Ref AWS::StackName
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, run the following command in order to CloudFormation provision the application resources&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation deploy \
--stack-name react-cors-spa-demo \
--template-file react-cors-spa-stack.yaml \
--region us-east-1 \
--no-fail-on-empty-changeset
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now, we're going to retrieve the values exposed by our CloudFormation template that are listed in the &lt;code&gt;Outputs&lt;/code&gt; section&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outputs:
BucketName:
Value: !Sub "react-cors-spa-${SimpleAPI}"
SPADomain:
Value: !GetAtt CFDistributionSPA.DomainName
APIDomain:
Value: !GetAtt CFDistributionAPI.DomainName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
Run the command&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws cloudformation describe-stacks \
--stack-name react-cors-spa-demo \
--region us-east-1 \
--query "Stacks[0].Outputs"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
Save the &lt;code&gt;BucketName&lt;/code&gt; for a future step&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build your React application. In the sample, we can achieve this by running &lt;code&gt;yarn build&lt;/code&gt; and locating the &lt;code&gt;out&lt;/code&gt; folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now we simply need to upload the content from the previous step, using the &lt;code&gt;BucketName&lt;/code&gt; from &lt;strong&gt;step 2&lt;/strong&gt;, to the AWS S3 Bucket&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync ./out "s3://$BUCKET_NAME"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
If you're using Next.js like the AWS Sample, you must also include the &lt;code&gt;_next_&lt;/code&gt; folder in your AWS S3 Bucket&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync ./_next "s3://$BUCKET_NAME"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now you know how to host your React SPA application in a fast, secure, and cost-effective stack inside the AWS Cloud.&lt;/p&gt;

&lt;p&gt;If you found this content helpful or have any questions about it, feel free to leave a comment below!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>react</category>
      <category>s3</category>
      <category>cloudformation</category>
    </item>
  </channel>
</rss>
