Danny Kim
Posted on October 9, 2023
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.
Here's how it works:
- Client sends requests to the proxy server.
- The proxy server decides which server should handle the request.
- The proxy server sends the request to the actual server and get the response.
- 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
- Nginx
- Vercel
- Firebase
- Netlify
- Cloudflare (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 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" }
]
}
Let's look into each rewrite rule more closely:
-
{ "source": "/", "destination": "https://example.webflow.io/" }
Assuming we deploy this site toexample.com
, Vercel will servehttps://example.webflow.io/
when a client requestshttps://example.com/
. -
{ "source": "/blog", "destination": "https://example.webflow.io/blog" }
Similar to above. It proxieshttps://example.com/blog
tohttps://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 tohttps://example.fly.dev/api/user
,https://example.com/api/user/1234
tohttps://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;
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'sdestination
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.
- For wildcard paths, imitate how Vercel handles wildcard substitution. Detect the
-
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.
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
November 26, 2024