Table of Contents

    Book an Appointment

    INTRODUCTION

    While working on a highly secure healthcare SaaS platform, we encountered a fascinating architectural constraint. The system required strict data isolation and network privacy. The architectural mandate was clear: the public entry point had to be an Application Load Balancer (ALB), and the frontend React Single Page Application (SPA) had to be hosted in a completely private Amazon S3 bucket. Access from the ALB to S3 was routed internally via an AWS PrivateLink VPC Interface Endpoint.

    During the initial deployment phase, the application loaded perfectly when users navigated from the root URL. However, a critical issue surfaced during User Acceptance Testing (UAT). Whenever testers refreshed the page on a nested route, or tried to access a direct deep link like `/dashboard/patient-records`, the application crashed, displaying a raw XML error from S3 stating “Access Denied” or “Key Not Found”.

    In a standard public architecture, this is easily fixed by using CloudFront or configuring S3 Website Hosting to use `index.html` as the Error Document. However, our strict compliance requirements prohibited the use of CDN edge caching, meaning CloudFront was completely off the table. This challenge required us to rethink how SPA routing behaves at the network layer in a fully private cloud topology. This article details how we diagnosed the limitations of S3 REST endpoints and implemented a robust, serverless solution, offering a blueprint for teams that need to deploy secure, private SPAs without relying on CDNs.

    PROBLEM CONTEXT

    To understand why this issue occurs, we must look at how SPA client-side routing interacts with web servers. In a React application utilizing React Router, the browser relies on a single HTML file (`index.html`). Once this file is loaded, JavaScript takes over and dynamically renders the appropriate components based on the URL path.

    When a user navigates from the home page to `/dashboard`, the transition happens entirely within the browser. The browser does not send a new request to the server for a `/dashboard` file. However, if the user explicitly types the URL `https://platform.domain.com/dashboard` into their browser, or refreshes the page, the browser issues an HTTP GET request to the ALB for the exact path `/dashboard`.

    In our architecture, the ALB was configured to forward all traffic to the IP addresses of the S3 VPC Interface Endpoint. The ALB faithfully forwarded the request for `/dashboard` directly to S3. Since S3 is an object store and not a traditional web server, it looked for a specific object named `dashboard` in the bucket. Because no such object existed, it returned an error. This is a common pitfall that organizations face when they hire react developers for spa deployment without aligning the frontend routing strategy with the backend infrastructure.

    WHAT WENT WRONG

    The root of the failure came down to the fundamental difference between S3 Website Endpoints and S3 REST API Endpoints.

    • S3 Website Endpoints: Support index document routing and custom error documents. If a file is not found, S3 can automatically serve `index.html` with a 200 OK status. However, Website Endpoints do not support VPC Endpoints (PrivateLink) and must be accessed over the internet or via public IP space.
    • S3 REST API Endpoints: Support VPC Endpoints and strict IAM/Bucket Policies, ensuring completely private connectivity. However, REST endpoints strictly serve exact object keys. They do not support the `ErrorDocument` configuration.

    Because we were mandated to use a VPC Interface Endpoint to maintain network privacy, we were forced to use the REST API endpoint. When the ALB forwarded the request for `/dashboard`, the S3 REST API evaluated the request, found no object matching that key, and immediately threw a 404 (or a 403 Access Denied, depending on ListBucket permissions). ALB simply passed this error back to the client.

    We realized that relying solely on ALB forwarding rules targeting S3 IP addresses would never work for deep linking in an SPA. We needed an intelligent routing layer capable of differentiating between static asset requests (like `.js`, `.css`, `.png`) and client-side application routes, rewriting the latter to serve `index.html`.

    HOW WE APPROACHED THE SOLUTION

    We evaluated several approaches to resolve this architectural bottleneck.

    Our first thought was to use ALB Advanced Routing Rules to issue redirects. While ALB can return custom HTTP responses or redirects, it cannot transparently rewrite a URL path and fetch a different file from the target group. A redirect would change the user’s browser URL back to the root `/`, destroying the deep link context.

    Our second option was deploying a fleet of Nginx reverse proxies on Amazon ECS Fargate behind the ALB. Nginx can easily use the `try_files $uri /index.html` directive. While highly effective, this introduced unnecessary compute overhead, requiring container maintenance, scaling policies, and additional infrastructure costs just to serve static files.

    Ultimately, we chose a serverless intelligent proxy approach using AWS Lambda as an ALB Target. We designed a configuration where the ALB handles path-based routing natively. Requests for known static assets are routed directly to the S3 VPC Endpoint, while all other requests (the application routes) fall back to a lightweight Lambda function. This function simply fetches `index.html` from the private S3 bucket and returns it. This strategy is precisely the kind of lean, secure engineering practice clients look for when they hire aws developers for private cloud architecture.

    FINAL IMPLEMENTATION

    To implement this, we restructured the ALB Listener Rules and deployed a lightweight Node.js Lambda function.

    1. ALB Listener Rule Configuration

    We configured the ALB listener with specific priority rules based on path patterns:

      • Rule 1 (Static Assets): IF Path matches `*.js`, `*.css`, `*.png`, `*.jpg`, `*.svg`, `*.woff2`, `*.json` -> THEN Forward to Target Group A (IP targets of the S3 VPC Interface Endpoint).
      • Rule 2 (Default Catch-All): IF Path does not match the above -> THEN Forward to Target Group B (AWS Lambda function).

    2. The Serverless Fallback Lambda

    The Lambda function acts as the SPA router. When it receives a request for a deep link like `/dashboard`, it uses the AWS SDK to securely fetch the `index.html` file from the private bucket and returns it as a formatted ALB response.

    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({ region: 'us-east-1' });
    exports.handler = async (event) => {
        const bucketName = process.env.PRIVATE_SPA_BUCKET;
        const indexKey = 'index.html';
        try {
            const params = {
                Bucket: bucketName,
                Key: indexKey
            };
            const s3Object = await s3.getObject(params).promise();
            const htmlContent = s3Object.Body.toString('utf-8');
            return {
                statusCode: 200,
                statusDescription: "200 OK",
                isBase64Encoded: false,
                headers: {
                    "Content-Type": "text/html; charset=utf-8",
                    "Cache-Control": "no-cache, no-store, must-revalidate"
                },
                body: htmlContent
            };
        } catch (error) {
            console.error("Error fetching index.html from S3:", error);
            return {
                statusCode: 500,
                statusDescription: "500 Internal Server Error",
                headers: { "Content-Type": "text/plain" },
                body: "Internal Server Error loading application."
            };
        }
    };
    

    3. Security and Performance Considerations

    Because the Lambda function executes within the VPC, it accesses the S3 bucket through the VPC Gateway/Interface Endpoint, keeping all traffic off the public internet. To ensure performance, we allocated 256MB of memory to the Lambda (providing a faster CPU slice) and utilized AWS SDK connection reuse. Since `index.html` is typically very small, the Lambda execution time averaged under 30ms. The heavy lifting for large static assets (compiled JS chunks, CSS, images) was handled directly by the ALB-to-S3 integration, bypassing Lambda entirely.

    LESSONS FOR ENGINEERING TEAMS

    Complex network constraints often reveal hidden limitations in seemingly simple technologies. Here are the key takeaways for teams designing secure SPA architectures:

    • Understand S3 Endpoint Distinctions: Never assume S3 REST API endpoints behave like S3 Website endpoints. `ErrorDocument` routing is strictly a feature of the Website endpoint and will not function over PrivateLink.
    • ALB is Not a Web Server: While powerful, ALBs lack the URL rewriting capabilities of Nginx or Apache. You cannot configure an ALB to transparently serve File A when File B is requested.
    • Separate Data from Routing: Treat your static assets separately from your SPA routing logic. Offloading static file serving directly to S3 via IP targets reduces compute load, while reserving custom logic only for route resolution.
    • Avoid Compute Overhead When Possible: Before deploying a fleet of containers just to run an Nginx proxy for static files, consider serverless edge or ALB-target patterns that require zero maintenance.
    • Embrace Network-Level Isolation: When you hire cloud developers for enterprise modernization, ensure they understand how to leverage VPC Endpoints, IAM condition keys, and ALB Target Groups to create zero-trust perimeters around static assets.

    WRAP UP

    Hosting a React SPA in a completely private environment without CloudFront requires navigating the limitations of static object storage. By combining ALB path-based routing with a lightweight Lambda fallback, we successfully implemented robust deep-linking and client-side routing while satisfying strict enterprise security mandates. This hybrid routing approach eliminated the need for maintaining proxy servers and kept all internal traffic entirely within the AWS network. If your organization is navigating complex cloud architecture constraints and you need to hire software developer expertise to build secure, scalable systems, contact us.

    Social Hashtags

    #PrivateReactSPA #AWSArchitecture #ReactJS #AmazonS3 #AWSLambda #ApplicationLoadBalancer #CloudSecurity #ServerlessArchitecture #SaaSArchitecture #CloudFrontAlternative #DevOps #AWSDevelopers

     

    Frequently Asked Questions