Hosting a single page application or website on S3
Tags: aws, s3, spa, cloudfront
Published: 2022-12-21 08:09:49

In this blog post I will walk you through how to configure a single page application or website. In a previous blog [Best practices for S3 web hosting and explaining why](https://binx.io/blog/2022/02/03/best-practices-for-s3-web-hosting-and-explaining-why/). [Tibor Hercz](https://binx.io/blog/author/tibor-hercz/) explained the best practices for hosting a static website on S3.
As Tibor explained, there are some downsides on using a `Origin Access Identity (OAI)`. But in some `Single Page Applications (SPA)` you will not experience this downside.

What is a single page application?
----------------------------------

A single page application loads a single web document. Based upon your interaction with this page the content is updated.  
The pages are rendered on your web browser. And more API calls are performed to fetch extra input. Due to this fact an SPA can be fast!
You can use the framework of your choice: [angular](https://angular.io), [reactjs](https://reactjs.org), [vue](https://vuejs.org), etc, etc. 

Setting up the infrastructure
-----------------------------

> For simplicity, I am providing CloudFormation snippets.

First we will need an S3 bucket to host the application in. We need an OAI. And we need to grant it access yo the bucket:

```yaml
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private

  MyOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: Allows CloudFront to reach the bucket

  MyBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - s3:GetObject*
              - s3:GetBucket*
              - s3:List*
            Effect: Allow
            Principal:
              CanonicalUser: !GetAtt MyOriginAccessIdentity.S3CanonicalUserId
            Resource:
              - !GetAtt MyBucket.Arn
              - !Sub ${MyBucket.Arn}/*
```

Next we will need the CloudFront distribution:

```yaml
  MyDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        CustomErrorResponses:
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /index.html
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
          TargetOriginId: origin
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: index.html
        Enabled: true
        HttpVersion: http2
        IPV6Enabled: true
        Origins:
          - ConnectionAttempts: 3
            ConnectionTimeout: 10
            DomainName: !GetAtt MyBucket.RegionalDomainName
            Id: origin
            S3OriginConfig:
              OriginAccessIdentity:
                Fn::Join:
                  - ""
                  - - origin-access-identity/cloudfront/
                    - Ref: MyOriginAccessIdentity
```

Once you have set up the infrastructure. You need to `build` your SPA. And upload the build to the S3 bucket.  
My assumption here is that you created an app with various routes/paths. 

How does it work?
-----------------

When you navigate to the hostname of the created CloudFront distribution. The distribution will fall back on the `index.html` file. This is because we have set the `DefaultRootObject` to `index.html`.  
When you navigate in the application it looks like the browser is changing paths. But in fact it is only updating the address in the address bar.
Let say you created a `/blog` route. You navigate to this route. And you do a hard refresh! You expect to get a `404 Not Found` error because there is no folder called `blog`.  
But because we configured a `CustomErrorResponses`. That will redirect all `404` errors to the `index.html` file with a `200` response code.
This will make sure that the Single Page Application gets loaded again. It will see the location in the address bar and renders the correct content.
This does mean that you will need to handle unknown routes in your application.

Conclusion
----------

When you want to host a Single Page Application you can use CloudFront with private buckets. You will get all the benefits of CloudFront while keeping the bucket private.