Single Page Apps (SPAs) are great. They can be hosted as a static site on object storage like an S3 bucket and served through a CDN like CloudFront to cache the content close to your users wherever they are in the world. All without having to manage server infrastructure.
There are however, things to keep in mind to make this setup work. The different frameworks these apps are built on have their own way of dealing with URLs. For example, React Router can intercept the request and handle it instead of sending it to the server.
This client-side routing means that React Router knows how to handle a request for www.example.com/products/12
while clearly there's no such resource as <bucket-name>/products/12
on the server. This all works fine as long as navigation happens within your app where React can intercept those requests. The only request the server should be handling is for www.example.com
or any other resource that actually lives on the server.
But what happens when you type www.example.com/products/12
in your browser's address bar and hit enter? That request is going to the server where it will make no sense because there's no resource by that URI. You might get something like this in your browser's console:
Refused to apply style from 'https://www.example.com/assets/index-12345abc.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
This and other errors have the same underlying cause: the resource your browser asked for is just not there. Depending on your setup, the server might be replying with the default document or something else.
We need to run some code at the edge locations that can intercept the request, rewrite the URI, and hand it over to CloudFront for further processing. If the requested resource is something the client should handle, set the URI to /
so the server will respond as expected and let the client figure out how to handle the request. If the requested URI is for a server resource like /img/cat.jpg
then don't change it. Fortunately, CloudFront Functions let us do precisely that. You write them in Javascript, with some restrictions. Here [1] is an example showing you how to create and test a function that looks like this:
function handler(event) {
var request = event.request;
var uri = request.uri;
var paths = ['assets', 'img', 'robots.txt', 'app.webmanifest']
var isServerPath = (path) => uri.includes(path);
if (!paths.some(isServerPath)) {
request.uri = '/';
}
return request;
}
In this example, assets/
is the folder where your bundler e.g. Vite places your app's Javascript and CSS files for production.
Once you have your CloudFront function working, it's time to associate it with your CloudFront distribution. This [2] will show you how to deploy your SPA as a secure static site with a CloudFront function handling requests as needed by React Router.
[1] https://github.com/santisbon/amazon-cloudfront-functions/tree/main/url-rewrite-single-page-apps
[2] https://github.com/santisbon/static-site