Lightweight Microfrontend Development With a Local Proxy Server

Nov 07, 2022 | By Roy Tinker

bridge with cars going fast

Introduction

Have you ever wished you didn't have to run your back-end services locally while developing locally on your front end? Well, I have good news for you! A local proxy server is most likely the solution to your problem.

In this post, we'll discuss what a proxy service is and how it helped us solve the same problem for our software engineers.

What Is a Proxy Service?

A proxy service listens at one or more ports on a machine and forwards communication to another machine. Theoretically, a proxy service can "stand in" for any network service.

How a proxy service works

The above image illustrates how a proxy service works. Client software connects to the proxy service, which forwards data to the server. When the server responds with data, the proxy service sends the data back to the client. Ideally, the proxy service is as transparent as possible, requiring little or no knowledge of its existence in client software.

Proxy services (and proxies in general) are an application of the indirection pattern, in other words, when you abstract the location of something. A post office box is a straightforward example of indirection: it allows people to contact you via an intermediary; as a result, you can change your home address independently of your mailing address. DNS services are another example of indirection: they abstract IP addresses out of URLs.

An old software aphorism states "We can solve any problem by introducing an extra level of indirection." (Attributed to David Wheeler). It's a half-joke since indirection introduces complexity and slows performance when overused; but otherwise, it's correct.

Problem and Solution

FloQast engineers prefer not to run more local services than are necessary. The front-end platform teams in particular need a way to avoid setting up and running back-end services for each product team they assist.

As you may be aware, we use a Microfrontend (MFE) architecture (here's a link to the most recent blog post in that series). For local development, we support two execution modes for working on MFEs: standalone and integrated. As it happened, we implemented two different types of proxy services that solve the local front-end development problem separately for these use cases.

1. Standalone Proxy

We'll start with the simpler of the two solutions. Our standalone proxy runs automatically inside the dev server for each microfrontend (when the MFE runs in standalone mode).

Did you know Create React App (CRA) has a built-in development proxy feature? You can read all about it in their documentation here.

In fact, using it can be as simple as adding a "proxy" key to your package.json file:

To tell the development server to proxy any unknown requests to your API server in development, add a proxy field to your package.json, for example:

"proxy": "http://localhost:4000",

We needed a somewhat more involved setup, so we went with the src/setupProxy.js method.

Like the setupProxy.js example linked in the CRA documentation, we used http-proxy-middleware and connected proxy middleware directly to the internal Express instance for individual MFEs. A little more complexity allows our engineers to optionally sub in one or more locally hosted lambdas (which are parsed at startup time from a REACT_APP_PROXY_EXCLUDE environment variable). If a request matches a local lambda, the proxy forwards the request to the local lambda service instead of to the dev environment.

To dodge potential CORS issues, make sure to include changeOrigin: true in your createProxyMiddleware call. You may also find you need to include cookieDomainRewrite: { '*': '' }:

app.use(
    '/api',
    createProxyMiddleware({
        target: `https://your.backend.environment`,
        changeOrigin: true,
        logLevel: 'debug',
        secure: false,
        cookieDomainRewrite: { '*': '' },
    })
);

2. Integrated Mode Proxy

We call our integrated mode proxy client-proxy. The core idea is simple. It's a node.js service that listens on each local port that normally hosts a local back-end service. It uses Express and http-proxy-middleware to proxy back-end requests to equivalent services running in a back-end environment.

Here's where it gets a little tricky. You may recall that our microfrontend "hub" (we call it client-hub) is what composes the app together in the browser.

Now, the "front door" of our app when running locally (without a proxy) is the same main service that listens on port 443 in production. Normally, when the browser makes its first request, the monolith internally requests the root HTML and JS files from the client-hub service in the cloud.

But if client-proxy proxies the root / request to the back-end monolith when doing local development, the monolith will return client-hub assets from the cloud -- which we don't want, since we're running client-hub and various MFEs locally.

So instead of forwarding the / request to the back end, client-proxy requests assets from the local client-hub and returns them to the browser. This diagram illustrates what's going on:

client proxy with no dev environment

There are several other tricks we make use of to return something different than what the monolith would return -- after all, client-proxy's services are each an Express app, so there's a lot of flexibility available.

Here's a simplified diagram describing the whole architecture when using client-proxy:

client proxy

Conclusion

Proxy services are an excellent solution for local development against cloud-hosted back-end services. For the most part, your code doesn't even need to know a proxy service is being used, since proxy services provide true indirection and thus increased flexibility.

The http-proxy-middleware node package is an excellent tool for HTTP proxying, and it can help you handle CORS issues through its changeOrigin and cookieDomainRewrite configuration options.

Integrated microfrontend development on your local machine doesn't need to keep you from using a proxy service. Wherever a network port has a listener, a proxy service can be substituted.

And finally, stay tuned for more development architecture posts! We're only just getting started on a vision to enable significant flexibility and productivity to developers for both front-end and back-end development.

Until next time! 👋

Roy Tinker
Roy is a Senior Software Engineer at FloQast. He enjoys building user interfaces that delight people and help them do their work. When he's not writing and maintaining code, he enjoys spending time with his family, reading, and grooving on the piano.

Check out research, videos, case studies, and more!

Learn more about working at FloQast!