Building a Full-Stack Serverless Application with Cloudflare Workers
One of my favorite developments in software development has been the advent of serverless. As a developer who has a tendency to get bogged down in the details of deployment and DevOps, it’s refreshing to be given a mode of building web applications that simply abstracts scaling and infrastructure away from me. Serverless has made me better at actually shipping projects!
That being said, if you’re new to serverless, it may be unclear how to translate the things that you already know into a new paradigm. If you’re a front-end developer, you may have no experience with what serverless purports to abstract away from you – so how do you even get started?
Today, I’ll try to help demystify the practical part of working with serverless by taking a project from idea to production, using Cloudflare Workers. Our project will be a daily leaderboard, called “Repo Hunt” inspired by sites like Product Hunt and Reddit, where users can submit and upvote cool open-source projects from GitHub and GitLab. You can see the final version of the site, published here.

Workers is a serverless application platform built on top of Cloudflare’s network. When you publish a project to Cloudflare Workers, it’s immediately distributed across 180 (and growing) cities around the world, meaning that regardless of where your users are located, your Workers application will be served from a nearby Cloudflare server with extremely low latency. On top of that, the Workers team has gone all-in on developer experience: our newest release, at the beginning of this month, introduced a fully-featured command line tool called Wrangler, which manages building, uploading, and publishing your serverless applications with a few easy-to-learn and powerful commands.
The end result is a platform that allows you to simply write JavaScript and deploy it to a URL – no more worrying about what “Docker” means, or if your application will fall over when it makes it to the front page of Hacker News!
If you’re the type that wants to see the project ahead of time, before hopping into a long tutorial, you’re in luck! The source for this project is available on GitHub. With that, let’s jump in to the command-line and build something rad.
Installing Wrangler and preparing our workspace
Wrangler is the command-line tool for generating, building, and publishing Cloudflare Workers projects. We’ve made it super easy to install, especially if you’ve worked with npm before:
npm install -g @cloudflare/wrangler
Once you’ve installed Wrangler, you can use the generate
command to make a new project. Wrangler projects use “templates” which are code repositories built for re-use by developers building with Workers. We maintain a growing list of templates to help you build all kind of projects in Workers: check out our Template Gallery to get started!
In this tutorial, we’ll use the “Router” template, which allows you to build URL-based projects on top of Workers. The generate
command takes two arguments: first, the name of your project (I’ll use repo-hunt
), and a Git URL. This is my favorite part of the generate
command: you can use all kinds of templates by pointing Wrangler at a GitHub URL, so sharing, forking, and collaborating on templates is super easy. Let’s run the generate
command now:
wrangler generate repo-hunt https://github.com/cloudflare/worker-template-router
cd repo-hunt
The Router template includes support for building projects with webpack, so you can add npm modules to your project, and use all the JavaScript tooling you know and love. In addition, as you might expect, the template includes a Router
class, which allows you to handle routes in your Worker, and tie them to a function. Let’s look at a simple example: setting up an instance of Router
, handling a GET
request to /
, and returning a response to the client:
// index.js
const Router = require('./router') addEventListener('fetch', event => { event.respondWith(handleRequest(event.request))
}) async function handleRequest(request) { try { const r = new Router() r.get('/', () => new Response("Hello, world!")) const resp = await r.route(request) return resp } catch (err) { return new Response(err) }
}
All Workers applications begin by listening to the fetch
event, which is an incoming request from a client to your application. Inside of that event listener, it’s common practice to call a handleRequest
function, which looks at the incoming request and determines how to respond. When handling an incoming fetch
event, which indicates an incoming request, a Workers script should always return a Response
back to the user: it’s a similar request/response pattern to many web frameworks, like Express, so if you’ve worked with web frameworks before, it should feel quite familiar!
In our example, we’ll make use of a few routes: a “root” route (/
), which will render the homepage of our site; a form for submitting new repos, at /post
, and a special route for accepting POST
requests, when a user submits a repo from the form, at /repo
.
Building a route and rendering a template
The first route that we’ll set up is the “root” route, at the path /
. This will be where repos submitted by the community will be rendered. For now, let’s get some practice defining a route, and returning plain HTML. This pattern is common enough in Workers applications that it makes sense to understand it first, before we move on to some more interesting bits!
To begin, we’ll update index.js to set up an instance of a Router
, handle any GET
requests to /
, and call the function index
, from handlers/index.js (more on that shortly):
// index.js
const Router = require('./router')
const index = require('./handlers/index') addEventListener('fetch', event => { event.respondWith(handleRequest(event.request))
}) function handleRequest(request) { try { const r = new Router() r.get('/', index) return r.route(request) } catch (err) { return new Response(err) }
}
As with the example index.js in the previous section, our code listens for a fetch
event, and responds by calling the handleRequest
function. The handleRequest
function sets up an instance of Router
, which will call the index
function on any GET
requests to /
. With the router setup, we route the incoming request, using r.route
, and return it as the response to the client. If anything goes wrong, we simply wrap the content of the function in a try/catch
block, and return the err
to the client (a note here: in production applications, you may want something more robust here, like logging to an exception monitoring tool).
To continue setting up our route handler, we’ll create a new file, handlers/index.js, which will take the incoming request and return a HTML response to the client:
// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const handler = () => { return new Response("Hello, world!", { headers })
}
module.exports = handler
Our handler
function is simple: it returns a new instance of Response
with the text “Hello, world!” as well as a headers
object that sets the Content-Type
header to text/html
– this tells the browser to render the incoming response as an HTML document. This means that when a client makes a GET
request to the route /
, a new HTML response will be constructed with the text “Hello, world!” and returned to the user.
Wrangler has a preview
function, perfect for testing the HTML output of our new function. Let’s run it now to ensure that our application works as expected:
wrangler preview
The preview
command should open up a new tab in your browser, after building your Workers application and uploading it to our testing playground. In the Preview tab, you should see your rendered HTML response:

With our HTML response appearing in browser, let’s make our handler
function a bit more exciting, by returning some nice looking HTML. To do this, we’ll set up a corresponding index
“template” for our route handler: when a request comes into the index
handler, it will call the template and return an HTML string, to give the client a proper user interface as the response. To start, let’s update handlers/index.js to return a response using our template (and, in addition, set up a try/catch
block to catch any errors, and return them as the response):
// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const template = require('../templates/index') const handler = async () => { try { return new Response(template(), { headers }) } catch (err) { return new Response(err) }
} module.exports = handler
As you might imagine, we need to set up a corresponding template! We’ll create a new file, templates/index.js, and return an HTML string, using ES6 template strings:
// templates/index.js
const template = () => { return <code><h1>Hello, world!</h1>`
} module.exports = template
Our template
function returns a simple HTML string, which is set to the body of our Response
, in handlers/index.js. For our final snippet of templating for our first route, let’s do something slightly more interesting: creating a templates/layout.js file, which will be the base “layout” that all of our templates will render into. This will allow us to set some consistent styling and formatting for all the templates. In templates/layout.js:
// templates/layout.js
const layout = body => `
<!doctype html>
<html> <head> <meta charset="UTF-8"> <title>Repo Hunt</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"> </head> <body>