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
- Getting Started with Serverless Cloudflare Workers
- Cloudflare Workers as a Web Server (with Webpack)
- Making API Calls From a Cloudflare Worker
- Key-Value Storage With Cloudflare Workers KV
- [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 🍻
- Join my 📰 Newsletter
- Subscribe to my 📺 YouTube, JoelCodes
- Say hi to me on Twitter @joelnet