About Reverse Proxy
Tags: network, webdev
Published: 2023-10-09 05:50:28

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnknvxfubi66x6cp79fz.png)

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:
- [Apache HTTP server](https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html)
- [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/)
- [Vercel](https://vercel.com/docs/edge-network/rewrites)
- [Firebase](https://firebase.google.com/docs/hosting/full-config)
- [Netlify](https://docs.netlify.com/routing/redirects/)
- [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) (this article is a good read about reverse proxy if you want to dig deeper into the concept.)

### 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](https://vercel.com/docs/edge-network/rewrites) in the config file.

```json
{
    "$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" }
    ]
}
```

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](https://webpack.js.org/configuration/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.


```js
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;
```

`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](https://github.com/chimurai/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.