TLDR - Routing with aws is hard
There are so many kinds of web applications. Backend, frontend, full-stack, e-commerce, blogs, serverless, etc, the list goes on. While they are all special and unique in their own right, there is a lot of very common functionality that needs to be implemented for a large majority of these applications.
To this day, accomplishing some of this common “boiler plate” functionality using major cloud providers remains shrouded in mystery…
Let’s begin our journey to full-stack (API + SPA) application routing on AWS.
We’re starting with a cloudfront distribution that has two origins, one is the s3 bucket (the frontend SPA) and the other is the backend API service. In this state routing works but we see 404 “key not found” errors from the frontend (at any route other than /index.html).
Cloudfront allows some custom error handling and it seems pretty straightforward, so I tried something like this:
This does the trick for the frontend, but now backend 404 errors are swallowed and instead we get a 200 response along with the frontend app index.
Since Cloudfront doesn’t allow that custom error behavior to be specified per origin, my next idea was to rely on the s3 bucket settings. I set an error page for the frontend:
This actually didn’t change any routing behavior, and I later learned that any bucket level rules are mostly ignored when using an s3 origin with cloudfront.
To work around that limitation, I replaced the s3 origin with a custom origin pointing to the website endpoint of the s3 bucket:
This seems closer to what we want:
The issue here is that while the frontend returns the index correctly, it still returns a 404 status code.
One thing I was trying to avoid here was adding unnecessary complexity…
After a decent amount of research I came to the conclusion that I can’t avoid using a Lambda function for this. Lambda functions can be used as sort of a “middleware” for cloudfront requests/responses. Since this is fairly common functionality that we’re trying to accomplish there were plenty of examples of what this lambda function would look like:
Wow. Just wow. So it turns out the body of the response is not exposed to the lambda function.
This means that we’ll need to replace the 404 status with a 200, AND make a request to fetch the frontend app index from the s3 bucket to use it to populate the response body.
I’m sure this would have worked but it just seemed a bit much. Lots of moving pieces to accomplish what I felt should be a fairly simple thing.
After some more research I learned that although the body of the response is not exposed to the lambda function, it will persist as long as the lambda function doesn’t modify the body in any way.
The final solution for me was a combination of attempt #2 and attempt #3. The main issue with #2 was that the frontend still returned a 404 status, so now the lambda function can be simplified to handle just the status:
This story is one of frustration and persistence. Don’t get me wrong, AWS is great and provides seemingly endless solutions to meet your infrastructure needs. These numerous solutions come with the caveat that the “best” or “correct” way isn’t always clear, even for problems that are far from unique (e.g. routing).
At coherence, we’re doing this kind of work across the development lifecycle so you can focus on what really matters, your actual application/business logic.