⛅ Cloudflare Workers as a Web Server with Webpack (Lesson 2)

⛅ Cloudflare Workers as a Web Server with Webpack (Lesson 2)

In this article, I will be configuring a Worker as a Web Server with Webpack.

This is the second article in a series I am doing on Cloudflare Workers. I am excited about the Cloudflare Workers platform and if you are too, subscribe to my Newsletter and get a notification for the next article!

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

Generate a Cloudflare Worker Website

If you already have wrangler installed, you can skip the npm install step.

$ npm install -g @cloudflare/wrangler
$ wrangler generate my-website
$ cd my-website

Configure Webpack

Webpack supports the webworker target output with this webpack.config.js.

I also move index.js to src/index.js. I like to keep all my source files in an src directory so they aren't hidden by all the configs in the root.

module.exports = {
    target: 'webworker',
    context: __dirname,
    entry: './src/index.js',
    mode: 'development',
    devtool: 'cheap-module-source-map',
    module: {
        rules: [
            {
                test: /\.html$/i,
                loader: 'html-loader',
            },
        ],
    },
}

I am also installing the html-loader to include html files in the bundle.

$ npm install --save-dev html-loader

Change The Worker Type to Webpack

Change the type to webpack and add webpack_config with the value webpack.config.js.

Don't forget to set theaccount_id.

name = "my-website"
type = "webpack"
account_id = "1234567890" # set the account_id here!
workers_dev = true
route = ""
zone_id = ""
webpack_config = "webpack.config.js"

Add HTML Files

Create an html directory and add an index.html and 404.html.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Hello Worker!</h1>
</body>
</html>

404.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>404 Not Found</h1>
</body>
</html>

Loading Routes

Modify the src/index.js file to import the HTML files and then in handleRequest, return either the index.html or the 404.html.

import index from '../html/index.html'
import notFound from '../html/404.html'

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Respond with hello worker text
 * @param {Request} request
 */
async function handleRequest(request) {
  const { pathname } = new URL(request.url)

  if (pathname === '/') {
    return new Response(index, {
      headers: { 'content-type': 'text/html' },
    })
  }

  return new Response(notFound, {
    headers: { 'content-type': 'text/html' },
    status: 404,
  })
}

At this point let's preview and make sure it's all working.

$ wrangler preview --watch

If all goes well, then a window should popup like this one showing our index.html page.

image.png

Smarter Routing

Routing with if statements is pretty primitive. I'm gonna improve this.

Copy the router.js from cloudflare / worker-template-router or (2) and paste it into src/lib/router.js.

Switch back to src/index.js and import the Router at the top of the file.

import Router from './lib/router'

Now I can change the routes inside handleRequest.

async function handleRequest(request) {
    const router = new Router()

    router.get(
        '/',
        () =>
            new Response(index, {
                headers: { 'content-type': 'text/html' },
            }),
    )
    router.all(
        () =>
            new Response(notFound, {
                headers: { 'content-type': 'text/html' },
                status: 404,
            }),
    )

    const response = await router.route(request)
    return response
}

It might not seem like a big change, but the route is now checking the path for / and the method for GET. This will give me great flexibility in my planned future.

Refactoring

At this stage the Website is working pretty well, but I always like to do a little bit of refactoring in the end. It just makes me happy.

Responses

First, I'm going to move the responses into a new file.

/**
 * src/lib/responses.js
 */
import notFoundHtml from '../../html/404.html'

export const htmlResponse = html =>
    new Response(html, {
        headers: { 'content-type': 'text/html' },
    })

export const notFoundResponse = () =>
    new Response(notFoundHtml, {
        headers: { 'content-type': 'text/html' },
        status: 404,
    })

Now I can import these from src/index.js

import { htmlResponse, notFoundResponse } from './lib/responses'

Then my handleRequest function cleans up like this.

async function handleRequest(request) {
    const router = new Router()

    router.get('/', () => htmlResponse(index))
    router.all(() => notFoundResponse(notFound))

    const response = await router.route(request)
    return response
}

Pages

I like having the concept of pages, similar to how Next.js works. All my logic can't be written inside of src/index.js and I like to break this out early.

So I'm going to create a pages directory and create my two routes.

/**
 * src/pages/index.js
 */
import { htmlResponse } from '../lib/responses'
import index from '../../html/index.html'

const home = () => htmlResponse(index)

export default home
/**
 * src/pages/404.js
 */
import { notFoundResponse } from '../lib/responses'

const notFound = () => notFoundResponse()

export default notFound

In src/index.js I import the new routes and remove some old imports.

import index from './pages/index'
import notFound from './pages/404'
// import index from '../html/index.html'
// import notFound from '../html/404.html'
// import { htmlResponse, notFoundResponse } from './lib/responses'

Then my handleRequest turns into this. Notice how I am passing the request into each page. It's unused now, but I will eventually have a route that will need it.

async function handleRequest(request) {
    const router = new Router()

    router.get('/', () => index(request))
    router.all(() => notFound(request))

    const response = await router.route(request)
    return response
}

Source Code

Check out the project over on my Github repo.

https://github.com/joelnet/cloudflare-worker-website

Summary

Configuring Cloudflare Workers to work as a web server is pretty simple using the webpack project type.

I was able to import HTML using the html-loader Webpack plugin and serve pages based on custom routing.

Subscribe to my Newsletter to continue learning about Cloudflare Workers!

Cheers 🍻