Usually, a web application consists of a frontend and a backend. With an SPA, the frontend can be a React app, for example, and built with WebPack. Then the backend can be a Lambda function.
The frontend URL is visible to users, so there is a strong push to bring that to a user-friendly domain (
example.com), for example, using CloudFront. But the API URL is hidden from users, only visible if they open the DevTools and inspect the outgoing requests. Because of this, it’s easier to use the generated domain name for the backend.
It works, but there are several problems with it.
The first is performance. When a visitor opens the site, the browser does a DNS resolution for the frontend domain. Then it establishes an HTTPS connection (TCP + TLS or QUIC) to fetch the assets (html, css, js). When the page is parsed it sends a request to the API, starting the whole process one more time: a DNS resolution to the API domain, then an HTTPS connection. This adds a noticable latency between showing the page and fetching the data.
But the bigger problem is that this setup requires a more complicated configuration.
First, the frontend needs to know the API URL. When the frontend assets are loaded, it needs to send an HTTP request to the backend. The API URL is usually hardcoded into the app when it is built. This adds one extra parameter that is different between different environments.
Second, the backend needs to know the frontend URL. Since the frontend and the backend are on different domains, when the browser fetches data from the API it sends a cross-origin request. To get access to the contents, the backend needs to respond with CORS headers, at least with the
This setup gets more complicated when the client needs to send credentials, such as a session cookie that identifies the user. In this case, the backend needs to include the
Access-Control-Allow-Credentials header, in which case the
Access-Control-Allow-Origin must not be
*. Since allowing all origins to send requests with credentials is a bad idea, the backend needs to validate that the request came from the frontend domain. In the case of multiple environments, this adds an extra environment variable to the API configuration.
Moving the two parts to a single domain
The above architecture implements domain-based (or host-based) routing as different domains host the different parts. To use a single domain, you can use path-based routing:
- when the path starts with
/api=> send the request to the API
- otherwise, send the request to the S3 bucket
With this setup, a single connection is enough to communicate with both the frontend and the backend. One DNS resolution and HTTP handshake is still needed, but the API connection can reuse the existing one. The net result is that the page will be populated with data faster once it’s loaded.
Also, a single-domain setup makes the requests same-origin. The frontend no longer needs to know where the API is for that specific environment: it’s
/api. And on the backend side, since the requests are not cross-origin, no CORS headers are needed. The backend no longer needs to know where the frontend is.
There is one thing to look out for: the request path in the API requests. With domain-based routing, the API could expect that all requests are realtive to the root path (
/). With path-based routing it’s no longer the case.
The solution is to use CloudFront Functions to change the request path.