Alex walks you through how to deploy a React App as an OpenFaaS Function.

Functions give a quick and easy way to add dynamic data to your React Apps with a predictable cost, but they can also host static React Apps too.

In this article, we’ll look at the various ways to deploy React Apps to OpenFaaS, whether that’s using a CDN for the front-end, multiple functions or a single function for everything.

I’ll also touch on costs, local debugging and how to share and collaborate with others. Live collaboration can save hours or even days when you have a tricky error in the Chrome console, and get someone else involved.

A front end served by a function?

React is JavaScript a library and ecosystem used to build front-end applications - web portals, news feeds, blogs, even mobile apps.

Developers usually write code in JSX format, which is transpiled into JavaScript and then served to the browser. The React app runs on the client side, and having a server-side component is completely optional.

Pictured: Amir Movahedi testing the OpenFaaS and Equinix Metal drone simulation for CES 2019, written with React.js.

This makes it a prime candidate for hosting in OpenFaaS. Any React App deployed as a function can take advantage of the ecosystem:

  • event-connectors from Kafka, AWS SQS, NATS, cron, etc
  • auto-scaling based upon CPU, RPS or inflight requests
  • simple deployment process and central management API
  • automated metrics collection and monitoring

There are two types of React apps that you may want to build:

  1. A static app with no backend, this could be deployed as a function, or to a CDN
  2. A static app with its own backend, potentially served by the same server process

Static apps

When a React App has no back-end API to call, making it into a function is a two-stage process:

  • Transpile the React JSX files into JavaScript and HTML files
  • Serve the static files to clients over HTTP

Static app with a backend

There certainly are use-cases for React Apps without any kind of back-end API, but it’s more popular to have some kind of persistent storage too. That’s where being able to call back into a server-side component makes a lot of sense.

Then you have three stages required:

  • Transpile the React JSX files into JavaScript and HTML files
  • Serve the static files to clients over HTTP
  • Server an API endpoint that runs the backend code.

Let’s take a look at a static app

First build a basic OpenFaaS function using the dockerfile template:

# Replace alexellis2 with your Docker Hub username
export OPENFAAS_PREFIX=alexellis2

faas-cli new --lang dockerfile portal

This creates:

portal.yml
portal/Dockerfile

See portal.yml:

version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080

functions:
  portal:
    lang: dockerfile
    handler: ./portal
    image: alexellis2/portal:latest

Whenever we run faas-cli build the file in ./portal/Dockerfile will be used to build a container image named: alexellis2/portal:latest.

We can ignore the Dockerfile for now and create the React App.

To create a new static app, first install Node.js 14 or higher and the npx tool (available with npm 5.2+).

Then generate a new app called portal:

# The create-react-app will fail due to the folder "portal"
# already existing, so create it in a new folder then move it back 
mkdir app
cd app
npx create-react-app portal

# Move the contents of app/portal into ./portal/

cd ..
mv app/portal/* ./portal
rm -rf app

Success! Created portal at /home/alex/go/src/github.com/openfaas/openfaas.github.io/portal/portal/portal
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

The structure will look like this:

  • portal.yml
  • portal/Dockerfile
  • portal/src/
  • portal/package.json
  • portal/build/
  • portal/node_modules/

Let’s try it out directly on our machine without OpenFaaS:

# Run this command from within the "portal" folder, alongside "package.json" and "Dockerfile"

npm start

Access it via http://127.0.0.1:3000

Edit the app

Edit the app’s source code to customise it

Now let’s write a Dockerfile to build the React app into static HTML, and then to serve it.

portal/Dockerfile

FROM ghcr.io/openfaas/of-watchdog:0.9.2 as watchdog

FROM node:16-alpine as build

WORKDIR /root/

# Turn down the verbosity to default level.
ENV NPM_CONFIG_LOGLEVEL warn

COPY package.json ./

RUN npm i --production

COPY src        ./src
COPY public     ./public

RUN NODE_ENV=production npm run build
RUN find build/

FROM alpine:3.14 AS runtime
WORKDIR /home/app/
RUN addgroup -S -g 1000 app && adduser -S -u 1000 -g app app

COPY --from=build /root/build /home/app/public
WORKDIR /home/app/public

COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
 
RUN chown app:app -R /home/app \
    && chmod 777 /tmp

USER app

ENV mode="static"
ENV static_path="/home/app/public"

ENV exec_timeout="10s"
ENV write_timeout="11s"
ENV read_timeout="11s"

HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1

CMD ["fwatchdog"]

The first line downloads the OpenFaaS watchdog. This will be used to serve to static HTML, CSS and JS files using a HTTP fileserver. Some people may use Nginx here instead, but it’s a little heavy-weight and specialised for our purposes.

Then we set up the Node.js version we need FROM node:16-alpine as build - the Alpine image is smaller, and ideal if we have no native npm modules to build like SQLite. Seeing as our code is only front-end based, we shouldn’t have to switch.

Notice that we copy package.json before any code. This is a trick to optimize the build. If you remove it then all the dependencies will be downloaded upon every build.

The RUN NODE_ENV=production npm run build step builds or “transpiles” the JSX files and other assets into a single directory that will be copied into the final image from /root/build to /home/app/public.

For the runtime image, we’re using FROM alpine:3.14 AS runtime which is a minimal Linux Operating System. Distroless could also work here, since the watchdog does all the work we need to serve the files. Note the ENV static_path="/home/app/public" value which tells the watchdog where to find the files.

Now run a build and test it out locally, with Docker:

# Buildkit enables a faster build process
export DOCKER_BUILDKIT=1

faas-cli build -f portal.yml

docker run --name portal-test \
    -p 8080:8080 \
    --rm -ti alexellis2/portal:latest

Now access it via port 8080, the content will be served by the built-in static webserver of the OpenFaaS watchdog: http://127.0.0.1:8080

Next, deploy the function to your local OpenFaaS cluster.

faas-cli up -f portal.yml

You’ll get a URL within a few seconds: http://127.0.0.1:8080/function/portal - this time, the function is being exposed via the OpenFaaS gateway.

It’s possible to add a custom domain whether you’re using OpenFaaS on Kubernetes or faasd. This maps a domain like portal.example.com to the function on the gateway to give your users a more friendly URL.

Example of a custom domain for a function

In this example, we see my sponsors’ benefits portal, a function written in Go which is mapped to “insiders.alexellis.io”. Users must authenticate with a valid GitHub account and are then authorized if they have a valid 25 USD / mo or higher subscription/sponsorship.

Get custom domains for yourself:

So what’s another real-world example of a React app served by OpenFaaS?

In 2020, whilst working on a client project for Equinix Metal, I built a webpage with mapbox to render the position of various drones. This formed part of a 5G demo at CES.

Have a look at the render-map function to see how it compares to what we built above.

Now, once you have your custom domain in place or have deployed to the OpenFaaS gateway, you will need to “mount” your React app at a different path.

Configure this via package.json and the homepage field.

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:8080",
  "homepage": "http://127.0.0.1:8080/function/myportal/app/"
}

If deploying to a custom domain, change the field accordingly.

The "proxy": "http://localhost:8080" field is useful for when you want to run the React app on your own machine, and have it make API calls to another function running on the same cluster.

Adding a backend

Some React Apps are useful enough without any dynamic data, but adding dynamic data is what can make them much more powerful. Functions are effectively shrunk down, stateless APIs, so we can use them to build a backend for React Apps.

We just need to decide how to serve the static HTML for the React App and our functions.

1. Deploy two functions

When deploying two functions, one will serve the static front-end and the other will serve the back-end API for any dynamic content needed.

For this option, we need to serve the function containing the front-end code and the backend functions on the same domain, or path. If the portal and function are served from different domains, the API call will be blocked due to Cross Origin Resource Sharing (CORS) protection.

  • openfaas.example.com/function/portal
  • openfaas.example.com/function/load-json

We can also remap the domain using a Kubernetes Ingress record or OpenFaaS FunctionIngress:

apiVersion: openfaas.com/v1
kind: FunctionIngress
metadata:
  name: ui-portal
  namespace: openfaas
spec:
  domain: "portal.example.com"
  path: "/ui/(.*)"
  function: "portal"
  ingressType: "nginx"
  tls:
    enabled: true
    issuerRef:
      name: "letsencrypt-prod"
      kind: "Issuer"
---
apiVersion: openfaas.com/v1
kind: FunctionIngress
metadata:
  name: load-json-v1-api
  namespace: openfaas
spec:
  domain: "portal.example.com"
  path: "/v1/load-json/(.*)"
  function: "load-json"
  ingressType: "nginx"
  tls:
    enabled: true
    issuerRef:
      name: "letsencrypt-prod"
      kind: "Issuer"
  • portal.example.com/ui/ => /function/ui
  • portal.example.com/v1/load-json/ => /function/load-json

As an alternative, you can edit the function to return a header to allow CORS requests.

See my blog post on the topic, where I show a static webpage served from GitHub Pages or another CDN, and a function running on OpenFaaS: Gain access to your functions with CORS

If you go for this option, you’ll probably want your two or more functions to be part of the same repository and share the same OpenFaaS YAML file.

Read how to append multiple functions into the same file: OpenFaaS YAML reference

2. A mixture of a CDN and OpenFaaS

For this option, we deploy the static site to a CDN, then make use of a CORS exception to allow it to call our OpenFaaS cluster to get dynamic data.

This approach means that the content for the React UI is served from a CDN, and we just use OpenFaaS functions for any dynamic data that we require. It’s a good mix, but if you’re already running an openfaas cluster, you could keep everything centralised and follow option 1.

As per above, see my blog post on the topic, where I show a static webpage served from GitHub Pages or another CDN, and a function running on OpenFaaS: Gain access to your functions with CORS

3. Serve the React app and API from the same function

For this option, we can adapt a template like node17 to serve the static content, and reply to certain API calls like GET /user/:id.

This means that we don’t have to consider CORS and that everything can be built, tested and deployed as one single unit.

React Apps are usually called Single Page Apps, so I’m calling this a “Single Function App” or SFA.

I’ve put together my own template here, which does a multi-stage build and uses Node.js for both the backend function and to build the React front-end. There’s no reason you couldn’t adapt it to use Go for the back-end or something else.

Create a new function using my custom template, and have it call itself:

# Change as required:
export OPENFAAS_PREFIX=docker.io/alexellis2

faas-cli template pull https://github.com/alexellis/node17-sfa
faas-cli new --lang node17-sfa myportal

You’ll see output as follows:

Function created in folder: myportal
Stack file written: myportal.yml

Notes:
You've created a Single Function App (SFA) using Node.js for your function code
and React JS for the front-end.

The "react" subfolder hosts your ReactJS app, edit the files in src/ and 
work with its package.json within that sub-directory.

The root folder contains your function's handler.js file and its separate 
package.json file.

Then, build and deploy the function:

faas-cli up -f myportal.yml

If you want to change the mounted path, edit: myportal/react/package.json

  "homepage": "/function/myportal/app/",

The homepage field must match the function name or custom domain you’re using to access the React app.

Edit your React app’s source code, and redeploy it:

Edit ./react/public/index.html or ./react/src/App.js

Then run:

faas-cli up -f myportal.yml
faas-cli describe -f myportal.yml myportal

Finally, access the function through it’s URL.

You can use axios to make requests to the function

cd react/
npm install --save axios

For example FunctionQuery.js:

import React from 'react';
import axios from 'axios';

export default class FunctionQuery extends React.Component {
  state = {
    functionRes: 'No result yet'
  }

  async componentDidMount() {
    let getURL = window.location.protocol
    +"//"+ window.location.host+`/`
    console.log(getURL)

    await axios.post(getURL, 
      {"input":"test",
       "window.location.host": window.location.host, 
       "user-agent": navigator.userAgent
      })
    .then(res => {
      const result = JSON.stringify(res.data);
      this.setState({functionRes: result});
    })
  }

  render() {
    return (
      <div>
        {this.state.functionRes}
      </div>
    )
  }
}

Then import the component into your React app in App.js:

import logo from './logo.svg';
import './App.css';
import FunctionQuery from "./FunctionQuery.js"

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />

        <FunctionQuery />
      </header>
    </div>
  );
}

export default App;

Then edit your function’s handler, so that it prints back the result of the function query:

'use strict'

module.exports = async (event, context) => {

  // Redirect any GET requests for the root path to the
  // React app.
  if(event.method == "GET" && event.path == "/") {
    return context
    .headers({"Location": "/app/"})
    .status(307)
    .succeed({})
  }

  // Any other requests are handled by our function below.
  const result = {
    'body': JSON.stringify(event.body),
    'content-type': event.headers["content-type"]
  }

  return context
    .status(200)
    .succeed(result)
}

If someone visits the root of the function in a web-browser, they’ll get redirected to the /app/ path where the React app is served.

After running faas-cli up, here’s how it looked for me when I shared my URL using an inlets tunnel. This also meant I could open a URL on my mobile phone over 4G, to test latency and responsiveness.

Function's result

The result from the function is displayed via the FunctionQuery.js component.

Why don’t you try deploying a function from the store, and calling that such as certinfo or nodeinfo? See faas-cli store list for more functions you can try out.

Wrapping up

We took a look at three ways that you can make use of React with OpenFaaS, one size doesn’t fit all, so I’ll leave my contact details and you can get in touch with questions or comments. I’ll now close with some thoughts on costs, local debugging and how to share your progress and get help from others.

The various approaches

  1. A static React App served from a CDN, like Netlify or GitHub Pages, which calls back to an OpenFaaS function for dynamic data via CORS.
  2. A static React App served from a function, which uses another function for dynamic data.
  3. A single Node.js function using my custom template which serves the static React App and calls itself to get dynamic data.

I’ve taken the time to list out each of the approaches, because one size does not fit all, and I’m sure you may have your own questions and comments too.

The costs

So how much does it cost to host an OpenFaaS function with any of the approaches above?

The answer is that you can start with a simple virtual machine (VM) from 5-10 USD / mo using our faasd project, but if you’re already running a Kubernetes cluster for other purposes, it could be effectively “free” since OpenFaaS doesn’t need many resources. To find out about more about faasd, check out this post: Meet faasd - portable Serverless without the complexity of Kubernetes or get started with my training package.

Local debugging

Local debugging is important with React apps, since there’s so much iteration you’ll want to do. Rather than deploying your function for every change, you can run it locally on your own computer. The React proxy is a simple way to do this.

Edit package.json, add:

"proxy": "http://127.0.0.1:8080"

See also setupProxy.js:

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(createProxyMiddleware('/api', {
       target: 'http://127.0.0.1:8080/api/',
       changeOrigin: true,
       pathRewrite: {
        '^/api/': '/'
      }    
    }));
};

This redirects any calls to /api to the local proxy on port 8080 on your local machine. So if you’re running a back-end there, you can run the react app with npm start and get the benefits of fast editing and live-reloading.

Then, cd into the “react” folder and run the following:

cd react/
npm install
npm start

The browser should open at: http://127.0.0.1:3000/

How do you share your progress with colleagues and early customers?

Sometimes I reach my limit of React or CSS knowledge and need a hand. I can simply open a secure HTTPS tunnel and have a friend or colleague check it out and give me direct feedback. This whole process takes less than a minute or two. I find using an inlets tunnel to be incredibly convenient for this.

I put up a public URL and was able to test the website on my phone and by sharing a link on Discord.

See also: Expose your local OpenFaaS functions to the Internet

Production builds

You can switch NODE_ENV used by npm run build by setting:

faas-cli up --build-arg NODE_ENV=production/dev

This can also be entered into your OpenFaaS stack file:

version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  myportal:
    lang: node17-sfa
    handler: ./myportal
    image: docker.io/alexellis2/myportal:latest
    build_args:
      NODE_ENV: production
      # NODE_ENV: dev

GitHub Actions provides a quick and easy way to build functions, find out how I build them here: Build at the Edge with OpenFaaS and GitHub Actions

Getting in touch

You can get in touch with me via Twitter: @alexellisuk or come along to the weekly OpenFaaS Office Hours

Alex Ellis

Founder of @openfaas.