This summer, Cloudflare announced that we were doubling the size of our Summer 2020 intern class. Like everyone else at Cloudflare, our interns would be working remotely, and due to COVID-19, many companies had significantly reduced their intern class size, or outright cancelled their programs entirely.
With our announcement came a huge influx of students interested in coming to Cloudflare. For applicants seeking engineering internships, we opted to create an exercise based on our serverless product Cloudflare Workers. I'm not a huge fan of timed coding exercises, which is a pretty traditional way that companies gauge candidate skill, so when I was asked to help contribute an example project that would be used instead, I was excited to jump on the project. In addition, it was a rare chance to have literally thousands of eager pairs of eyes on Workers, and on our documentation, a project that I've been working on daily since I started at Cloudflare over a year ago.
In this blog post, I will explain the details of the full-stack take home exercise that we sent out to our 2020 internship applicants. We asked participants to spend no more than an afternoon working on it, and because it was a take home project, developers were able to look at documentation, copy-paste code, and generally solve it however they would like. I'll show how to solve the project, as well as some common mistakes and some of the implementations that came from reviewing submissions. If you're interested in checking out the exercise, or want to attempt it yourself, the code is open-source on GitHub. Note that applications for our internship program this year are closed, but it's still a fun exercise, and if you're interested in Cloudflare Workers, you should give it a shot!
What the project was: A/B Test Application
Workers as a serverless platform excels at many different use-cases. For example, using the Workers runtime APIs, developers can directly generate responses and return them to the client: this is usually called an originless application. You can also make requests to an existing origin and enhance or alter the request or response in some way, this is known as an edge application.
In this exercise, we opted to have our applicants build an A/B test application, where the Workers code should make a request to an API, and return the response of one of two URLs. Because the application doesn’t make request to an origin, but serves a response (potentially with some modifications) from an API, it can be thought of as an originless application – everything is served from Workers.
Client <-----> Workers application <-------> API
|-------> Route A
|-------> Route B
A/B testing is just one of many potential things you can do with Workers. By picking something seemingly “simple”, we can hone in on how each applicant used the Workers APIs – making requests, parsing and modifying responses – as well as deploying their app using our command-line tool wrangler. In addition, because Workers can do all these things directly on the edge, it meant that we could provide a self-contained exercise. It felt unfair to ask applicants to spin up their own servers, or host files in some service. As I learned during this process, Cloudflare Workers projects can be a great way to gauge experience in take home projects, without the usual deployment headaches!
To provide a foundation for the project, I created my own Workers application with three routes - first, an API route that returns an array with two URLs, and two HTML pages, each slightly different from the other (referred to as "variants").
With the API in place, the exercise could be completed with the following steps:
Make a fetch request to the API URL (provided in the instructions)
Parse the response from the API and transform it into JSON
Randomly pick one of the two URLs from the array
variants
inside of the JSON responseMake a request to that URL, and return the response back from the Workers application to the client
The exercise was designed specifically to be a little past beginner JavaScript. If you know JavaScript and have worked on web applications, a lot of this stuff, such as making fetch requests, getting JSON responses, and randomly picking values in an array, should be things you're familiar with, or have at least seen before. Again, remember that this exercise was a take-home test: applicants could look up code, read the Workers documentation, and find the solution to the problem in whatever way they could. However, because there was an external API, and the variant URLs weren't explicitly mentioned in the prompt for the exercise, you still would need to correctly implement the fetch request and API response parsing in order to give a correct solution to the project.
Here's one solution:
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
// URL returning a JSON response with variant URLs, in the format
// { variants: [url1, url2] }
const apiUrl = `https://cfw-takehome.developers.workers.dev/api/variants`
const random = array => array[Math.floor(Math.random() * array.length)]
async function handleRequest(request) {
const apiResp = await fetch(apiUrl)
const { variants } = await apiResp.json()
const url = random(variants)
return fetch(url)
}
When an applicant completed the exercise, they needed to use wrangler to deploy their project to a registered Workers.dev subdomain. This falls under the free tier of Workers, so it was a great way to get people exploring wrangler, our documentation, and the deploy process. We saw a number of GitHub issues filed on our docs and in the wrangler repo from people attempting to install wrangler and deploy their code, so it was great feedback on a number of things across the Workers ecosystem!
Extra credit: using the Workers APIs
In addition to the main portion of the exercise, I added a few extra credit sections to the project. These were explicitly not required to submit the project (though the existence of the extra credit had an impact on submissions: see the next section of the blog post), but if you were able to quickly finish the initial part of the exercise, you could dive deeper into some more advanced topics (and advanced Workers runtime APIs) to build a more interesting submission.
Changing contents on the page
With the variant responses being returned to the client, the first extra credit portion asked developers to replace the content on the page using Workers APIs. This could be done in two ways: simple text replacement, or using the HTMLRewriter API built into the Workers runtime.
JavaScript has a string .replace
function like most programming languages, and for simple substitutions, you could use it inside of the Worker to replace pieces of text inside of the response body:
// Rewrite using simple text replacement - this example modifies the CTA button
async function handleRequestWithTextReplacement(request) {
const apiResponse = await fetch(apiUrl)
const { variants } = await apiResponse.json()
const url = random(variants)
const response = await fetch(url)
// Get the response as a text string
const text = await response.text()
// Replace the Cloudflare URL string and CTA text
const replacedCtaText = text
.replace('https://cloudflare.com', 'https://workers.cloudflare.com')
.replace('Return to cloudflare.com', 'Return to Cloudflare Workers')
return new Response(replacedCtaText, response)
}
If you’ve used string replacement at scale, on larger applications, you know that it can be fragile. The strings have to match exactly, and on a more technical level, reading response.text()
into a variable means that Workers has to hold the entire response in memory. This problem is common when writing Workers applications, so in this exercise, we wanted to push people towards trying our runtime solution to this problem: the HTMLRewriter API.
The HTMLRewriter API provides a streaming selector-based interface for modifying a response as it passes through a Workers application. In addition, the API also allows developers to compose handlers to modify parts of the response using JavaScript classes or functions, so it can be a good way to test how people write JavaScript and their understanding of APIs. In the below example, we set up a new instance of the HTMLRewriter, and rewrite the title
tag, as well as three pieces of content on the site: h1#title
, p#description
, and a#url
:
// Rewrite text/URLs on screen with HTML Rewriter
async function handleRequestWithRewrite(request) {
const apiResponse = await fetch(apiUrl)
const { variants } = await apiResponse.json()
const url = random(variants)
const response = await fetch(url)
// A collection of handlers for rewriting text and attributes
// using the HTMLRewriter
//
// https://developers.cloudflare.com/workers/reference/apis/html-rewriter/#handlers
const titleRewriter = {
element: (element) => {
element.setInnerContent('My Cool Application')
},
}
const headerRewriter = {
element: (element) => {
element.setInnerContent('My Cool Application')
},
}
const descriptionRewriter = {
element: (element) => {
element.setInnerContent(
'This is the replaced description of my cool project, using HTMLRewriter',
)
},
}
const urlRewriter = {
element: (element) => {
element.setAttribute('href', 'https://workers.cloudflare.com')
element.setInnerContent('Return to Cloudflare Workers')
},
}
// Create a new HTMLRewriter and attach handlers for title, h1#title,
// p#description, and a#url.
const rewriter = new HTMLRewriter()
.on('title', titleRewriter)
.on('h1#title', headerRewriter)
.on('p#description', descriptionRewriter)
.on('a#url', urlRewriter)
// Pass the variant response through the HTMLRewriter while sending it
// back to the client.
return rewriter.transform(response)
}
Persisting variants
A traditional A/B test application isn't as simple as randomly sending users to different URLs: for it to work correctly, it should also persist a chosen URL per-user. This means that when User A is sent to variant A, they should continue to see Variant A in subsequent visits. In this portion of the extra credit, applicants were encouraged to use Workers' close integration with the Request
and Response
classes to persist a cookie for the user, which can be parsed in subsequent requests to indicate a specific variant to be returned.
This exercise is dear to my heart, because surprisingly, I had no idea how to implement cookies before this year! I hadn't worked with request/response behavior as closely as I do with the Workers API in my past programming experience, so it seemed like a good challenge to encourage developers to check out our documentation, and wrap their head around how a crucial part of the web works! Below is an example implementation for persisting a variant using cookies:
// Persist sessions with a cookie
async function handleRequestWithPersistence(request) {
let url, resp
const cookieHeader = request.headers.get('Cookie')
// If a Variant field is already set on the cookie...
if (cookieHeader && cookieHeader.includes('Variant')) {
// Parse the URL from it using regexp
url = cookieHeader.match(/Variant=(.*)/)[1]
// and return it to the client
return fetch(url)
} else {
const apiResponse = await fetch(apiUrl)
const { variants } = await apiResponse.json()
url = random(variants)
response = await fetch(url)
// If the cookie isn't set, create a new Response
// passing in all the information from the original response,
// along with a Set-cookie header, setting the value `Variant`
// to the randomly selected variant URL.
return new Response(response.body, {
...resp,
headers: {
'Set-cookie': `Variant=${url}`,
},
})
}
}
Deploying to a domain
Workers makes a great platform for these take home-style projects because the existence of workers.dev and the ability to claim your workers.dev subdomain means you can deploy your Workers application without needing to own any domains. That being said, wrangler and Workers do have the ability to deploy to a domain, so for another piece of extra credit, applicants were encouraged to deploy their project to a domain that they owned! We were careful here to tell people not to buy a domain for this project: that's a potential financial burden that we don't want to put on anyone (especially interns), but for many web developers, they may already have test domains or even subdomains they could deploy their project to.
This extra credit section is particularly useful because it also gives developers a chance to dig into other Cloudflare features outside of Workers. Because deploying your Workers application to a domain requires that it be set up as a zone in the Cloudflare Dashboard, it's a great opportunity for interns to familiarize themselves with our onboarding process as they go through the exercise.
You can see an example Workers application deploy to a domain, as indicated by the wrangler.toml
configuration file used to deploy the project:
name = "my-fullstack-example"
type = "webpack"
account_id = "0a1f7e807cfb0a78bec5123ff1d3"
zone_id = "9f7e1af6b59f99f2fa4478a159a4"
Where people went wrong
By far the place where applicants struggled the most was in writing clean code. While we didn't evaluate submissions against a style guide, most people would have benefitted strongly from running their code through a "code prettifier": this could have been as simple as opening the file in VS Code or something similar, and using the "Format Document" option. Consistent indentation and similar "readability" problems made some submissions, even though they were technically correct, very hard to read!
In addition, there were many applicants who dove directly into the extra credit, without making sure that the base implementation was working correctly. Opening the API URL in-browser, copying one of the two variant URLs, and hard-coding it into the application isn't a valid solution to the exercise, but with that implementation in place, going and implementing the HTMLRewriter/content-rewriting aspect of the exercise makes it a pretty clear case of rushing! As I reviewed submissions, I found that this happened a ton, and it was a bummer to mark people down for incorrect implementations when it was clear that they were eager enough to approach some of the more complex aspects of the exercise.
On the topic of incorrect implementations, the most common mistake was misunderstanding or incorrectly implementing the solution to the exercise. A common version of this was hard-coding URLs as I mentioned above, but I also saw people copying the entire JSON array, misunderstanding how to randomly pick between two values in the array, or not preparing for a circumstance in which a third value could be added to that array. In addition, the second most common mistake around implementation was excessive bandwidth usage: instead of looking at the JSON response and picking a URL before fetching it, many people opted to get both URLs, and then return one of the two responses to the user. In a small serverless application, this isn't a huge deal, but in a larger application, excessive bandwidth usage or being wasteful with request time can be a huge problem!
Finding the solution and next steps
If you're interested in checking out more about the fullstack example exercise we gave to our intern applicants this year, check out the source on GitHub: https://github.com/cloudflare-internship-2020/internship-application-fullstack.
If you tried the exercise and want to build more stuff with Cloudflare Workers, check out our docs! We have tons of tutorials and templates available to help you get up and running: https://workers.cloudflare.com/docs.