Production-Ready AWS CloudFront for SPA
Tags: aws, cloud, webdev, devops
Published: 2023-02-03 15:19:09

In this article, we will discuss the basic scenario for using **[CloudFront](https://aws.amazon.com/cloudfront/?nc1=h_ls)** to serve a **static SPA**.

_As always, we will try to make it not just work but to achieve the maximum performance available in AWS. So, I'd like to start with a basic scenario and then try to improve it._


## Static files
If you work in the **AWS** environment, you usually upload static files to **S3**.

You need to configure **Bucket Policy** to enable public access (without authorization):

```yaml
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-website-bucket-static/*"
        }
    ]
}
```
And you need to enable **Static website hosting**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hcflsjh4vwgn6ab6tc2k.png)


And that's it! Now your site is available by the url: 
`http://<bucket-name>.s3-website-<region>.amazonaws.com/`

## HTTPS
The apparent drawback of the current solution is the lack of **HTTPS**. So let's set up [**CloudFront**](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html) to resolve this.

Technically we only need to specify the correct [CloudFront **Origin**](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6rervyrh36mga14q4oak.png)

When the distribution is deployed, your site will be available by CloudFront [DNS](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html) name, e.g.: 
`https://21nnj7ewt8yqwc.cloudfront.net`

_The most important advantage of using CF is that you will get a full-fledged CDN, i.e., your static files will be delivered from the location closest to you, which means that the delays in the website loading will be minimal._

## SPA
You will soon notice that if you have SPA, routing will not work. E.g.:
`https://21nnj7ewt8yqwc.cloudfront.net/test/route`
Because CloudFront will try to redirect requests to the `test/route` folder, which does not exist in S3.

## SPA: First attempt to fix
In some posts or answers, you may find the following solution: configure **Error document** for S3 Static website hosting to redirect failed attempts back to index.html:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmaocf01ds5j6xcp0ufc.png)

It will work, but you will have below issues:
1. All requests will have a **404** error code
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8y7mder9anyk4ibb0sf.png)
2. They will be served not from the cache (CF will request content on each request from S3)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y46wida5k9b7kugpi2pu.png)
3. S3 will request the content twice, first at the path specified in the request, and after an error, index.html itself

## SPA: Second attempt to fix
The most popular solution that you could find on the internet: configure CloudFront with **Custom Error Response**. In our case, it would be:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwd1d361a8kxq6ggbzki.png)

And it will fix the issue with the **404** error code:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwcfryp7oon61rscg7sv.png)

 
But unfortunately, you will still have the issue with the cache and two requests. The reason for this is simple: CloudFront will request the page from S3, and when it receives a 404, it will return index.html from the cache. So you will lose all the advantages of **CDN**.

## SPA: Solution
To fix both issues, we can implement [CloudFront Function](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html) and fix routing for our SPA.

Just create a new Function:
```javascript
function handler(event) {
  var request = event.request;
  var uri = request.uri;
  if (uri.endsWith('/')) {
    request.uri = '/index.html';
  } 
  else if (!uri.includes('.')) {
    request.uri = '/index.html';
  }
  return request;
}
```
And attach it to the CloudFront:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6vg57bvdlu1i5ae4q8li.png)

Now you will have the correct response code, and all requests will be served from the cache:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7jiiy91hvqs5o3vdsabv.png)

There are no penalties for launching these functions. They run in the exact location where the CDN runs.

## S3 cache policy
You can configure some cache parameters on the S3 level. But you should do this very carefully. For example, if you take example from popular [S3 sync plugin](https://github.com/k1LoW/serverless-s3-sync) for **Serverless Framework** and upload **index.html** with below configuration:
```yaml
params:
  - index.html:
    CacheControl: 'no-cache'
```
You will force CloudFront to request this date on each request:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5afsc48vdi2yv1swp60e.png)
Which, firstly, will work slowly, and secondly, it will load Origin with unnecessary requests.

The disadvantage of not having this setting is that you must make an invalidation request in the AWS console or via CLI / API to update the cached content.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1bd5t8l6rssqglms5fer.png)

# API
If you have an API inside AWS infrastructure, please read my previous posts about [Origin Shield](https://dev.to/roman_abdulmanov/improve-aws-cloudfront-api-performance-with-origin-shield-4al6) and [GraphQL](https://dev.to/roman_abdulmanov/production-ready-graphql-for-aws-serverless-54be). 

By combining these approaches, you will achieve the best performance results and a higher level of security.

Thanks!