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.
Drones flying around my hometown 😃.
— Amir Movahedi (@qolzam) December 26, 2019
Try packet labs IoT workshop take drones around your hometown😍 . @Packethost @openfaas #IoT #drone pic.twitter.com/rN99h692nL
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:
- A static app with no backend, this could be deployed as a function, or to a CDN
- 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’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.
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:
- For faasd users, see Serverless For Everyone Else for how to set up a custom domain.
- For Kubernetes users, you can use an additional Kubernetes Ingress record or out helper for that called FunctionIngress
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.
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
- 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.
- A static React App served from a function, which uses another function for dynamic data.
- 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'm writing up an article on @reactjs with @OpenFaaS.
— Alex Ellis (@alexellisuk) February 28, 2022
Here's a template "node17-react" that I've been iterating on. It contains both a backend API and the React app itself.@inletsdev provided a very convenient preview URL with HTTPS
Let me know if you'd like to try it out pic.twitter.com/j6ScbhGOVN
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