DEV Community

Arnaud
Arnaud

Posted on • Originally published at keypup.io

Deploy your SPA and programmatically manage traffic with Cloudflare Workers

TL;DR; Checkout Cloudflare Workers for deploying Single Page Applications (SPA) or handling your traffic programmatically.

I previously wrote about how we deploy our backend services on GCP. This week we'll talk about frontend deployment and traffic handling. For this we will use Cloudflare workers.

Cloudflare Workers are a serverless environment allowing you to deploy JS or WebAssembly logic to Cloudflare's edge endpoints across the world.

Not only does this allow you to easily distribute your SPA but you will also benefit from Cloudflare's anti-DdoS features and be able to manage your traffic programmatically.

For this article we will assume the following:

  • Your app is hosted under https://app.mydomain.com
  • The app.mydomain.com DNS is already pointing to Cloudflare
  • Your API backend is hosted on a PaaS at xyz-us.saas.net
  • The SPA and the API must share the same domain for cookie sharing reasons
  • Traffic going to https://app.mydomain.com/api/* must go to your backend API
  • Traffic going to https://app.mydomain.com/auth/* must go to your backend API
  • Traffic going to https://app.mydomain.com/* must go to your SPA

With the requirements above you will need two things:

  • The ability to compile and deploy your SPA
  • The ability to route traffic going to https://app.mydomain.com to the SPA or the API backend based on path matching.

Prerequisite

For this tutorial you will need npm setup on your machine as well as wrangler.

Wrangler's is Cloudflare's CLI for Cloudflare Workers. You can install it by running:

# Install wrangler
npm install -g @cloudflare/wrangler

# Setup your API token. This will open a web page asking you to login to Cloudflare
wrangler login

# Confirm your are logged in
wrangler whoami
Enter fullscreen mode Exit fullscreen mode

Introduction to Cloudflare Workers

Cloudflare Workers are a JS/WebAssembly serverless runtime allowing you to run any kind of HTTP-based application. Workers pushed to Cloudflare get deployed to all edge locations (100+ across the world).

The most minimal application looks like this:
A minimal Cloudflare Worker

You do not need to write code directly on the Cloudflare console to deploy applications. You can actually compile any JS app and push it to Cloudflare workers using the wrangler CLI.

If you wish to learn more about the Cloudflare Worker's runtime API, feel free to have a look at their doc. We will be using some of their runtime API in the second part of this article (the router).

If you wish to explore building workers, feel free to have a look at their starter apps.

Deploying your (React) SPA

Using wrangler, this is fairly straightforward.

Note that I use React as an example because this is what we use at Keypup. But any JS application can be deployed using the steps below.

Go to your React app folder and initialize a Cloudflare Worker project for your app:

wrangler init --site
Enter fullscreen mode Exit fullscreen mode

This step will generate two assets:

  • A workers-site folder with bootstrap worker code to invoke your app
  • A wrangler.toml file to configure the settings of your app worker

Update the wrangler.toml file to reflect your Cloudflare configuration. Only fill the name and account_id for now.

# This is the name of your application. In this case the app will be published under
# a Clouflare-generated domain that looks like: https://my-app.my-worker-domain.workers.dev
name = "my-app"

# Which build strategy to use: webpack, javascript, and rust. Keep webpack.
type = "webpack"

# Your Cloudflare account id, which you can find by going to your Cloudflare Workers' page.
account_id = ""

# If set to true you app will be deployed under your *.workers.dev domain (as
# my-app.my-worker-domain.workers.dev).
#
# If you set it to false then you will have to specify the 'route' or 'routes' attribute
# using your production domain such as https://app.mydomain.com/*
workers_dev = true

# Can be left empty for now as we will deploy to the *.workers.dev domain
route = ""
# routes = []

# Can be left empty for now as we will deploy to the *.workers.dev domain
# Can be used to specify a domain ID when deploying to a production domain (e.g. *.mydomain.com)
zone_id = ""

[site]
# The directory containing your static assets (output of your build)
bucket = "./build"

# The location of the worker entry-point. Leave untouched.
entry-point = "workers-site"
Enter fullscreen mode Exit fullscreen mode

Once you are done just build and deploy your app:

# Build your app
npm run build
# Or
yarn build

# Publish your app to Cloudflare workers
wrangler publish
Enter fullscreen mode Exit fullscreen mode

That's it!

Your React app is now available at https://my-app.my-worker-domain.workers.dev

Routing traffic

When it comes to routing traffic to your SPA and your backend there are two options:

  1. Leverage the native routing of Cloudflare (DNS + Worker routing)
  2. Build our own router using another Cloudflare Worker.

We prefer to use option (2) because it gives us more flexibility in terms of programmatically controlling routing rules but I will still show you what option (1) looks like for completeness purpose.

Option 1: DNS and worker routing

The simplest approach for SPA + backend routing is using the native routing functionalities provided by Cloudflare DNS and Workers.

First we configure Cloudflare DNS to point app.mydomain.com to xyz-us.saas.net (our backend API). As such it will forward all traffic to your backend. This is not what we want just now but it will act as a default and will allow us to configure bypass routes when we are done with the worker routing part.

Now update the wrangler.toml file and specify that your SPA should receive all traffic:

# This is the name of your application.
name = "my-app"

# Which build strategy to use: webpack, javascript, and rust. Keep webpack.
type = "webpack"

# Your Cloudflare account id, which you can find by going to your Cloudflare Workers' page.
account_id = ""

# We do not want the worker to be deployed on the dev domain
workers_dev = false

# We want our SPA to receive all traffic by default
route = "app.mydomain.com/*"

# You need to fill the zone id for the mydomain.com zone
zone_id = ""

[site]
# The directory containing your static assets (output of your build)
bucket = "./build"

# The location of the worker entry-point. Leave untouched.
entry-point = "workers-site"
Enter fullscreen mode Exit fullscreen mode

Re-deploy your application using wrangler:

wrangler publish
Enter fullscreen mode Exit fullscreen mode

All traffic to app.mydomain.com is now forwarded to your SPA.

Now let's configure bypass rules so that /api and /auth traffic is actually routed to the original DNS (the backend).

Go to the Cloudflare Workers page and add two routes to bypass Workers for /api/* and /auth/*. Ensure the Worker dropdown is set to None.

Bypass rule for /api

Bypass rule for /auth

That's it! Your app is now configured to send all traffic to your Cloudflare-hosted SPA except for the /api and /auth endpoints which are pointing to the original DNS (your backend)

It's a bit counterintuitive to setup two default routes followed by exclusion rules, but it's the only way - as far as I know - to ensure wildcard traffic gets routed to the SPA eventually.

Now let's explore the other alternative: building a custom router.

Option 2: Custom routing using a Worker

In this section, we will leave your SPA on the Cloudflare dev domain and ask Cloudflare to direct all traffic to a routing Worker which will then decide where traffic should be forwarded.

If you have modified the wrangler.toml file of your SPA in the previous section, make sure to reset it to the dev version:

# This is the name of your application. In this case the app will be published under
# a Clouflare-generated domain that looks like: https://my-app.my-worker-domain.workers.dev
name = "my-app"

# Which build strategy to use: webpack, javascript, and rust. Keep webpack.
type = "webpack"

# Your Cloudflare account id, which you can find by going to your Cloudflare Workers' page.
account_id = ""

# If set to true you app will be deployed under your *.workers.dev domain (as
# my-app.my-worker-domain.workers.dev).
#
# If you set it to false then you will have to specify the 'route' or 'routes' attribute
# using your production domain such as https://app.mydomain.com/*
workers_dev = true

# Can be left empty for now as we will deploy to the *.workers.dev domain
route = ""
# routes = []

# Can be left empty for now as we will deploy to the *.workers.dev domain
# Can be used to specify a domain ID when deploying to a production domain (e.g. *.mydomain.com)
zone_id = ""

[site]
# The directory containing your static assets (output of your build)
bucket = "./build"

# The location of the worker entry-point. Leave untouched.
entry-point = "workers-site"
Enter fullscreen mode Exit fullscreen mode

Deploying your router

Use wrangler to create a new worker project:

# Generate new worker project using the cloudflare-provided router template
wrangler generate app-router https://github.com/cloudflare/worker-template-router

# Navigate to the newly created folder
cd app-router
Enter fullscreen mode Exit fullscreen mode

Replace the index.js of the project with the following logic. The logic below tells the router to proxy traffic to /api and /auth to our backend API and all other traffic to our SPA:

const Router = require('./router')

// -------------------------------------------------
// Registration logic
// -------------------------------------------------
// Declare router
const RT = new Router();

// Helper function used to register route handlers
// See Routing rules section
const proxyTo = hostname => request => {
    // Point to backend
    const url = new URL(request.url);
    const forwardedHost = url.hostname;
    url.hostname = hostname;

    // Build request. Keep track of the original Host.
    const req = new Request(url, request);
    req.headers.append('X-Forwarded-Host', forwardedHost);

    // Execute request
    return fetch(req);
}

// -------------------------------------------------
// Configuration
// -------------------------------------------------
const SPA_HOST = 'my-app.my-worker-domain.workers.dev'
const API_HOST = 'xyz-us.saas.net'

// -------------------------------------------------
// Routing rules
// -------------------------------------------------
RT.any('app.mydomain.com/api/*', proxyTo(API_HOST))
RT.any('app.mydomain.com/auth/*', proxyTo(API_HOST))
RT.any('app.mydomain.com/*', proxyTo(SPA_HOST))

// -------------------------------------------------
// Handler
// -------------------------------------------------
async function handleRequest(request) {
    const resp = await RT.route(request);
    return resp;
}

// Entrypoint
addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})
Enter fullscreen mode Exit fullscreen mode

Update the wrangler.toml of the project to tell Cloudflare that all traffic to app.mydomain.com should be handled by your router:

name = "app-router"
type = "webpack"
account_id = "you-account-id"
zone_id = "your-zone-id"
routes = [
  "app.mydomain.com/*"
]
Enter fullscreen mode Exit fullscreen mode

Now publish your newly created router using wrangler:

wrangler publish
Enter fullscreen mode Exit fullscreen mode

That's it! Your traffic is now programmatically routed to your SPA and backend API by the app router.

Testing locally

It is possible to test your routing rules locally using wrangler in development mode.

Use the following command:

wrangler dev --host=app.mydomain.com
Enter fullscreen mode Exit fullscreen mode

Then visit http://localhost:8787 to test your logic locally and ensure traffic is routed as expected.

Note: Cloudflare headers are not present when testing locally. If your routing logic relies on these you'll need to add them manually in you local requests (using curl or Postman).

Going beyond simple routing

You are now in full control of the routing logic to your application. This means you can:

Manage multiple domains (just add domains to the routes array in your wrangler.toml)

  • Stitch multiple backend services together under the same domain
  • Route traffic based on IP addresses or source country
  • Inject custom headers in the requests
  • ...and more!

Here is an example of doing country-specific routing for your backend API:

const Router = require('./router')

// -------------------------------------------------
// Configuration
// -------------------------------------------------
const SPA_HOST = 'my-app.my-worker-domain.workers.dev'
const API_HOSTS = {
  US: 'xyz-us.saas.net',
  FR: 'xyz-fr.saas.net'
}

// -------------------------------------------------
// Registration logic
// -------------------------------------------------

// Declare router
const RT = new Router();

// Helper function used to register route handlers
// See Routing rules section
const proxyTo = hostname => request => {
    // Point to backend
    const url = new URL(request.url);
    const forwardedHost = url.hostname;
    url.hostname = hostname;

    // Build request. Keep track of the original Host.
    const req = new Request(url, request);
    req.headers.append('X-Forwarded-Host', forwardedHost);

    // Execute request
    return fetch(req);
}

// Handler for backend requests based on country
const backendProxy = request => {
  // Extract request information
  const url = new URL(request.url);
  const forwardedHost = url.hostname;

  // Select destination host based on country
  // Default to US if no backend configured for that specific country
  const country = request.headers.get('cf-ipcountry');
  const backend = API_HOSTS[country] || API_HOSTS['US'];
  url.hostname = backend;

  // Build request. Keep track of the original Host.
  const req = new Request(url, request);
  req.headers.append('X-Forwarded-Host', forwardedHost);

  // Execute request
  return fetch(req);
}

// -------------------------------------------------
// Routing rules
// -------------------------------------------------
RT.any('app.mydomain.com/api/*', backendProxy)
RT.any('app.mydomain.com/auth/*', backendProxy)
RT.any('app.mydomain.com/*', proxyTo(SPA_HOST))

// -------------------------------------------------
// Handler
// -------------------------------------------------
async function handleRequest(request) {
    const resp = await RT.route(request);
    return resp;
}

// Entrypoint
addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})
Enter fullscreen mode Exit fullscreen mode

I recommend you look at the Cloudflare Worker examples to get a feel of what you can achieve.

Wrapping up

Cloudflare Workers allow you to not only deploy your SPA(s) but also take control of your whole application facade.

Their serverless approach combined with the fact that workers are deployed on their worldwide-distributed edge endpoints make it a very efficient way to manage entrypoint traffic as a whole.

If you find yourself constrained by your current traffic management capabilities I recommend you give Cloudflare Workers a try.

Top comments (0)