⛅ Smart Routing with Cloudflare Workers and Webpack (bonus content)

⛅ Smart Routing with Cloudflare Workers and Webpack (bonus content)

Automatic Smart Routing for Cloudflare Workers simplifies routing and removes boilerplate. Currently each route is setup manually. This has been tedious and needs to change.

Cloudflare Workers Course Outline

  1. Getting Started with Serverless Cloudflare Workers
  2. Cloudflare Workers as a Web Server (with Webpack)
  3. Making API Calls From a Cloudflare Worker
  4. Key-Value Storage With Cloudflare Workers KV
  5. [Bonus] Smart Routing with Cloudflare Workers

Before Smart Routing

This is what my routes look like before smart routing. Multiple imports as well as routes to manage inside of src/index.js.

// ❌ Every page is imported at the top
import allPeople from './pages/allPeople'
import index from './pages/index'
import person from './pages/person'
import files from './pages/files'
import filesPost from './pages/files.post'
import filesList from './pages/files.list'

// ❌ Every route is manually added
router.get('/', () => index(request))
router.get('/people/?', () => allPeople(request))
router.get('/people/.+', () => person(request))
router.get('/files/.+', () => files(request))
router.get('/files/?', () => filesList(request))
router.post('/files/?', () => filesPost(request))

I don't like this, there's far too much boilerplate. Fortunately with some features of webpack, this can be improved.

Also, In the previous lessons, I used a very crude method to pull parameters off the routes. A route like /people/123 would be parsed and 123 would become id.

// ❌ Gross
const person = async request => {
  const url = new URL(request.url)
  const id = url.pathname.substring(8)
  const person = await fetchPerson(id)
  /* code */
}

What I Want Instead

It would be nice to add files to the project and not have to worry about routing. I want the pages and routes to just be picked up automatically and modify less files.

Something like this (below), where the route is included in the page and not in index.js.

/**
  * ./src/pages/status.js
  */ 
import { textResponse } from '../lib/responses'

// ✅ routing is automatic
export const route = '/status/?'

const status = () => textResponse('OK')

export default status

It would also be nice if the parameterization of routes be be more idiomatic. Something like this:

// ✅ Route Parameters
const person = async ({ params }) => {
  const person = await fetchPerson(params.id)
  /* code */
}

Webpack's require.cache

Webpack keeps a cache of all files added to the project inside of require.cache. This can be exploited to build the routes automatically.

Because webpack's require.cache contains all files and not just the pages, the non-pages need to be filtered out. So the test that seems to work for this is a page must be a Module and also export a route.

This is what those tests look like:

const isModule = module => {
  if (!module || !module.exports) return false
  return Object.prototype.toString.call(module.exports) === '[object Module]'
}

const hasRoute = module => {
  if (!module || !module.exports) return false
  return !!module.exports.route
}

Next, export getRoutes. This function filters for the route pages and returns an Array of routes that include a route, method (default to get), and the module.

export const getRoutes = () =>
  Object.values(require.cache)
    .filter(module => isModule(module) && isRoute(module))
    .map(module => ({
      route: module.exports.route,
      method: module.exports.method || 'get',
      module: module.exports.default,
    }))

Auto-include Files With Webpack

Once the imports for the pages are removed from index.js, Webpack won't include them. So they have to be forced into the bundle.

This can be done by modifying the webpack.config.js and adding the files there.

The glob function returns a list of all the JavaScript files in the ./src/pages/ directory.

const glob = require('glob')

const files = glob.sync('./src/pages/**/*.js')

Include the files in the webpack's entry.

module.exports = {
  entry: files.concat(['./src/index.js']),
}

If there's a better way, let me know in the comments.

Add The Auto-Routing to src/index.js

Add the auto-routing before any catch-all routes. In this case, the notFound route should be included last.

import Router from './lib/router'
import notFound from './pages/404'
import { getRoutes } from './lib/auto-routes'

const routes = getRoutes()
const router = new Router()

routes.forEach(({ route, method, module }) => router[method](route, module))
router.all(notFound)

Creating Route Parameters

In the previous lessons, I used a very crude method to pull parameters off the routes. A route like /people/123 would be parsed and 123 would become id.

// ❌ Gross
const person = async request => {
  const url = new URL(request.url)
  const id = url.pathname.substring(8)
  const person = await fetchPerson(id)
  /* code */
}

It would be nice if I could do something like this instead:

// ✅ Route Parameters
const person = async ({ params }) => {
  const person = await fetchPerson(params.id)
  /* code */
}

This feature can be added by modifying the src/lib/router.js file to assign a params object onto the req. Find Path and add the one-liner like below.

const Path = regExp => req => {
  const url = new URL(req.url)
  const path = url.pathname
  const match = path.match(regExp) || []
  // 👇 Insert this line to create the `params` on `req`.
  req.params = match && match.groups ? match.groups : {}
  return match[0] === path
}

Since routes are already RegEx, we can use the Named Group feature for this.

// ❌ No Named Group
export const route = '/people/.+'

// ✅ Group Named "id"
export const route = '/people/(?<id>.+)'

Now code like this is possible!

// 🔥 Route Parameters
const person = async ({ params }) => {
  const person = await fetchPerson(params.id)
  /* code */
}

Summary

These improvements will simplify the the way new routes are created. It also extracts parameters from the routes.

Browse the repository at this point in history.

Subscribe to my Newsletter to continue learning about Cloudflare Workers!

Cheers 🍻