About Reverse Proxy

0916dhkim

Danny Kim

Posted on October 9, 2023

About Reverse Proxy

Being able to stitch together a website with multiple services is a powerful technique. With this technique, teams don't need to use one single tool to build every page on their websites. Designers can iterate on the landing page using Webflow without affecting the dev team, the customer success team can edit the helpdesk items independently, and all those pages look cohesive to the main app from users' perspective!

This is done through a thing called reverse proxy.

What is Reverse Proxy?

Reverse proxy is a server that sits in front of all other servers so that users can't tell which server is serving each content. A reverse proxy makes it feel like everything comes from the same domain. Please see the following diagram.

Reverse proxy diagram

Here's how it works:

  1. Client sends requests to the proxy server.
  2. The proxy server decides which server should handle the request.
  3. The proxy server sends the request to the actual server and get the response.
  4. The proxy server sends the response to the client.

Then why do we call this reverse proxy? It's because forward proxy is a middleman for clients. Reverse proxy is reverse because it is a middleman for servers.

Of course, we can have more than 2 servers behind the proxy!
If we run multiple servers of the same type behind a proxy, that's called load-balancer. We can stitch together multiple types of servers with a proxy too.

Example: Webflow + React

Shall we take a look at a more concrete example? In this post, I will present a use-case of combining two websites under the same domain. One built with Webflow, and the other built with React.

Webflow is a no-code website builder that allows designers to directly implement their designs into pages. React is all-code (?) tool that allows us to build sophisticated features on web.

I'll use Vercel, React, and Webpack in this post, but you can setup reverse proxy with many different technologies. You can use any of the below:

Vercel

When you bundle a React app with Webpack, the result is a HTML file, a JS file, and static assets. Vercel is (among other things) a platform to host these files + some capabilities for proxy.

We deploy a site on Vercel by uploading static files (e.g. js bundle, html, css, images) and a config file named vercel.json. By default, Vercel will only serve the static files we uploaded. We can tell it to proxy some requests by defining rewrites in the config file.

{
    "$schema": "https://openapi.vercel.sh/vercel.json",
    "scope": "our-site",
    ...
    "rewrites": [
        { "source": "/", "destination": "https://example.webflow.io/" },
        { "source": "/blog", "destination": "https://example.webflow.io/blog" },
        { "source": "/api/:path(.*)", "destination": "https://example.fly.dev/api/:path" },
        { "source": "/(.*)", "destination": "/_index.html" }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Let's look into each rewrite rule more closely:

  • { "source": "/", "destination": "https://example.webflow.io/" } Assuming we deploy this site to example.com, Vercel will serve https://example.webflow.io/ when a client requests https://example.com/.
  • { "source": "/blog", "destination": "https://example.webflow.io/blog" } Similar to above. It proxies https://example.com/blog to https://example.webflow.io/blog.
  • { "source": "/api/:path(.*)", "destination": "https://example.fly.dev/api/:path" } Now this is Vercel's syntax for defining wildcard proxy. https://example.com/api/user will be proxied to https://example.fly.dev/api/user, https://example.com/api/user/1234 to https://example.fly.dev/api/user/1234, etc.
  • { "source": "/(.*)", "destination": "/_index.html" } A couple things to mention here.
    • This catch-all proxy rule is required for serving a single-page-application. You can read more about what is required when configuring servers for client-side-routing: https://create-react-app.dev/docs/deployment/#serving-apps-with-client-side-routing
    • Static files takes precedence over rewrite rules on Vercel. So if there is /index.html file, it will take precedence over a rewrite rule for the / route. That's why we name our index page _index.html for the SPA here. We want to make sure the / route to be proxied to the webflow page.

That's all that's needed for configuring proxy for our Vercel deployment!

Webpack

One problem: our local dev environment doesn't make use of Vercel proxy. Webpack dev server has a separate way of configuring reverse proxy for local environment.

Here's an example webpack.config.js. Explanation is below the code snippet.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const vercel = require("../vercel.json");

/** @type {import("webpack").Configuration} */
const config = {
    mode: "development",
    devtool: "inline-source-map",
    devServer: {
        port: 3000,
        open: false,
        hot: true,
        historyApiFallback: {
            index: "/_index.html",
        },
        proxy: buildProxyFromVercelJson(vercel.rewrites),
        devMiddleware: {
            index: false, // for root proxy
        },
    },
    entry: "./src/index.tsx",
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: "babel-loader",
                exclude: /node_modules/,
            },
            {
                test: /\.(ts|tsx)$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ["*", ".js", ".jsx", ".ts", ".tsx"],
    },
    plugins: [
        new HtmlWebpackPlugin({
            // Usually HtmlWebpackPlugin will generate an index.html file,
            // but we need to use a different filename so that we can reverse proxy the root route on Vercel.
            // Vercel will serve the index.html file if it exists instead of proxying to the landing page.
            template: "./public/_index.html",
            favicon: "./public/favicon.svg",
            filename: "_index.html",
        }),
    ],
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js",
        publicPath: "/",
    },
};

/**
 * Transforms the Vercel rewrite config into a webpack dev server proxy config.
 * @param  {{source: string, destination: string}[]} rewriteConfig Vercel rewrite config
 * @returns Equivalent webpack dev server proxy config
 */
function buildProxyFromVercelJson(rewriteConfig) {
    const proxyConfig = [];
    for (const rule of rewriteConfig) {
        if (rule.source === "/(.*)") {
            // skip the default rewrite
            continue;
        }
        const SOURCE_WILD_CARD_REGEX = /\/:path\(\.\*\)$/;
        const DESTINATION_WILD_CARD_REGEX = /\/:path$/;
        const isWildCard = SOURCE_WILD_CARD_REGEX.test(rule.source);
        if (isWildCard) {
            proxyConfig.push({
                context: rule.source.replace(SOURCE_WILD_CARD_REGEX, "/"),
                target: rule.destination.replace(DESTINATION_WILD_CARD_REGEX, ""),
                pathRewrite: (path) => {
                    const re = `^${rule.source.replace(SOURCE_WILD_CARD_REGEX, "(.*)")}$`; // example: /api/:path(.*) -> ^/api/(.*)$
                    return path.match(re)[1];
                },
                changeOrigin: true,
            });
        } else {
            proxyConfig.push({
                context: (path) => path === rule.source,
                target: rule.destination,
                pathRewrite: () => "",
                changeOrigin: true,
            });
        }
    }
    return proxyConfig;
}

module.exports = config;
Enter fullscreen mode Exit fullscreen mode

HtmlWebpackPlugin is responsible for generating a bundled HTML file. We configure it to output to _index.html instead of the default filename index.html because Vercel needs it for / route proxy as mentioned in the previous section.

config.devServer.historyApiFallback.index should be set to /_index.html. This configuration is essentially doing the same thing as Vercel's { "source": "/(.*)", "destination": "/_index.html" } rule. It enables our SPA to work on multiple paths.

Also, config.devServer.devMiddleware should be set to false in order for the root route proxy to take effect. The dev server returns the SPA's HTML if the option is set to true.

Finally, we convert rewrite rules from vercel.json config file into config.devServer.proxy using an utility function called buildProxyFromVercelJson.

buildProxyFromVercelJson

Let's dissect what's going on within this utility function.
The function is a big for-loop that iterates over each rewrite rules from Vercel config. It translates each rule to what webpack-dev-server can parse and return the resulting array.

The first thing we do in the for-loop is skipping the rule for /(.*) because that part is handled by config.devServer.historyApiFallback option.

Then we translate each rule. webpack-dev-server uses http-proxy-middleware under the hood, and here's how to translate each field.

  • context: This function determines whether the rule applies to the given path or not.
    • For wildcard paths, match all paths that begins with the source route.
    • For non-wildcard paths, check if the path is exactly equal to the source route.
  • target: Proxied location. It corresponds to Vercel's destination option.
  • pathRewrite: This function determines how to translate the source route's path to the destination path.
    • For wildcard paths, imitate how Vercel handles wildcard substitution. Detect the :path(.*) part of the source path and append it at the end of the destination route.
    • For non-wildcard paths, use the destination route exactly as-is.
  • changeOrigin: Always true.

With this webpack config, npx webpack serve will spin up a dev server with reverse proxy. Opening http://localhost:3000/ on a browser will show the webflow page!

Conclusion: The Chimera Website

In this example, we stitched together Webflow and a React app together. The same technique can be applied to add many more things behind the proxy. Like a blog, documentation, online store, CDN for large files (images, video, software packages).

The key point is to keep the experience consistent between the deployed website & the local dev environment. Vercel + Webpack is just an example, and you should be able to apply this pattern to other platforms & tools as well!

Notes
buildProxyFromVercelJson utility function doesn't support all Vercel rewrite wildcard syntax. It only matches when the wildcard is :path(.*) at the end of the route.

💖 💪 🙅 🚩
0916dhkim
Danny Kim

Posted on October 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related