Published on

How to Build a URL Shortener with Cloudflare Workers and KV Storage

Authors

Introduction

In this tutorial, you will be building a URL shortener with Cloudflare workers and Cloudflare KV.

Cloudflare Workers is a platform for deploying serverless code to run on the same edge network that powers Cloudflare's CDN.

After following this tutorial, you will have a fully working URL shortener and be ready to build your own apps with Cloudflare Workers.

You can try a demo of the URL shortener you will build here!

Prerequisites

To complete this tutorial, you will need

Step 1 — Installing and Logging into Wrangler

Install wrangler globally with the following command:

npm install -g @cloudflare/wrangler

Run the following command to confirm that it is successfully installed:

wrangler -V

You should get an output like the following.

Output
wrangler 1.12.3

Now wrangler is installed, Authenticate wrangler with your Cloudflare account with the following command:

wrangler login

You will be asked to allow wrangler to open a page in the browser. Type Y and press ENTER. After logging into your account and granting wrangler access, wrangler will now be authenticated. In the next step, you will bootstrap your worker.

Step 2 — Creating the Cloudflare Worker App

We will create our app from the Cloudflare Workers default template. Open a terminal at the directory where you want the app to be created.

Run this command in the terminal to create the Worker app:

wrangler generate short-linker https://github.com/cloudflare/worker-template

This creates a worker app called short-linker for you from the default Cloudflare Worker template on GitHub.

Enter the directory of the worker app with

cd short-linker

There are a number of files in the newly created short-linker folder. The two main files you will be working with will be the index.js file which is the entry point of the worker and the wrangler.toml which contains configuration data for the worker.

Step 3 — Starting the Development Server

Before you can start a development server, you have to add your Cloudflare account id to wrangler.toml.

Run the following command to find your account id:

wrangler whoami

Your Account ID will be displayed beside your account name. Copy your Account ID from the console.

Edit the line with account_id in wrangler.toml, replacing my-account-id with the account ID you copied.

wrangler.toml
name = "short-linker"
type = "javascript"
account_id = "my-account-id"
workers_dev = true
route = ""
zone_id = ""

Start the development server with the following command.

wrangler dev

This creates a preview deployment that is accessible from http://localhost:8787.

Run the following command in the terminal to make a request your worker

curl http://localhost:8787

You should have this output

Output
Hello Worker

An alternative way to test the worker is to visit http://localhost:8787 in your browser.

If you face any errors, make sure you added your Account ID to wrangler.toml.

Understanding the Worker Script

Open index.js. The contents should look as below

index.js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})
/**
 * Respond with hello worker text
 * @param {Request} request
 */
async function handleRequest(request) {
  return new Response('Hello worker!', {
    headers: { 'content-type': 'text/plain' },
  })
}

At the top, we have an event listener listening for fetch events. In that event listener, we pass the event object to a handler handleRequest that returns a response based on the data contained in the object passed to it.

The handleRequest function simply returns a Response object with a body and some headers.

Replace Hello worker with Hi worker in index.js and save.

When you save, the development server will automatically detect the changes and redeploy the changed code.

Preview your changes by running

curl http://localhost:8787

The result will now change to this.

Output
Hi worker

Understanding the Request Object

The request object that is passed to the handleRequest function contains all the data in the request body. This includes the URL that the request was made to, the method of the request, the body of the request, some special objects added at the Cloudflare edge, etc.

Some of the request object's most useful properties and methods are

  • request.method which is the method of the request e.g. "POST", "GET", "UPDATE"
  • request.url which is the URL where the request was made to.
  • request.text() which returns the request body as plain text
  • request.json() returns the request body as a json object

You can find a full list of the properties of the request object at the documentation page for Request.

All requests to our worker regardless of the path or method will pass through handleRequest. You will use these properties and methods of request to determine the worker's response. The default worker, for example, returns the same response regardless of the path you visit.

A request to http://localhost:8787/test-path, gets the same response as one to http://localhost:8787.

To vary the response based on the URL path, you will have to write design your code to give varying responses based on the value of request.url. Similarly, to run separate logic for POST and GET requests to the same URL, you have to write a conditional statement that handles the case when request.method == "POST" and when request.method == "GET".

You can test that the responses are the same, regardless of method or path by running these curl commands from your command line

curl -X GET http://localhost:8787/some/sub/folder
curl -X POST http://localhost:8787
curl -X POST http://localhost:8787/api
curl -X GET http://localhost:8787

To illustrate the idea of changing the response based on the request method, modify index.js to look as follows:

index.js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})
/**
 * Respond with hello worker text
 * @param {Request} request
*/
async function handleRequest(request) {
  if(request.method == "POST") {
    return new Response('Hello POST worker!', {
      headers: { 'content-type': 'text/plain' },
    })
  }
  return new Response('Hi worker', {
    headers: { 'content-type': 'text/plain' },
  })
}

Run the following command from a terminal to make a POST request to the worker:

curl -X POST http://localhost:8787

You will have the following response

Output
Hello POST worker

Run the following command too

curl -X GET http://localhost:8787

The response remains the same as before

Output
Hi worker

The code modification checks if the request is a POST request, and if so, returns "Hello POST worker" and returns "Hi worker" instead. You should now have a good understanding of what the request object is used for.

You will now proceed to build the URL shortener.

How the URL Shortener Will Work

We will start by defining some terms that we will be using:

  • Destination URL - this refers to the URL that a visitor will be redirected to after visiting the short URL.
  • Random string - the random string refers to the randomly generated end of a short URL. For example, the random string in http://localhost:8787/abc123 is abc123.

When you make a POST request to http://localhost:8787, with the body of the request containing the URL you want to redirect to, the worker will generate a random string, for example abc123. We will store a map from this random string to the destination URL contained in the request body. On a visit to http://locahost:8787/abc123, we will extract the random string at the end, find what destination that string is tied to, and finally redirect to that URL.

We need some form of persistent storage to store the random strings and the URL the string is tied to. Cloudflare KV is a perfect fit for this need. Cloudflare KV is a key-value storage just like the Redis database. A key-value is similar to a big dictionary where you can assign values to keys, and also read values for keys.

Step 4 — Creating a Namespace

In Cloudflare KV, a collection of keys and values is known as a namespace. A namespace is what you will call a table in a relational database, only that it has two columns - one for the keys and another for the values.

Create a namespace called URL_SPACE with the following command:

wrangler kv:namespace create "URL_SPACE"

You will be prompted to add some extra values to the kv_namespaces array in wrangler.toml.

Output
Creating namespace with title "short-linker-URL_SPACE"
 Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "URL_SPACE", id = "943543435ac8e2a7a4c6b89fe343c0c13a02" }

Open wrangler.toml and update the needed lines. Replace namespaceid with the id that is given.

wrangler.toml
...
kv_namespaces = [
    { binding = "URL_SPACE", id = "namespaceid" }
]
...

What this does is that it binds a URL_SPACE variable in our worker to this namespace. This means we can write to the newly created namespace in our worker with URL_SPACE.put("key", "value") and read a value with URL_SPACE.get("key").

Since we are working on a preview deployment, we have to make an extra modification to kv_namespaces.

Edit the kv_namespaces array in wrangler.toml and add the preview_id.

wrangler.toml
name = "short-linker"
type = "javascript"
account_id = "my-account-id"
workers_dev = true
route = ""
zone_id = ""
kv_namespaces = [
    { binding = "URL_SPACE", id = "namespaceid", preview_id = "namespaceid" }
]

You will receive a warning for using the same namespace for production and preview deploys but you can ignore it for now. This is because the value of id is the namespace id of the production namespace and the value of preview_id is the id of the namespace that will be used during development.

The option to use different namespaces in different environments is there to allow developers to work locally with a separate namespace from the one deployed to production and avoid accidentally breaking production.

Step 5 — Creating an Endpoint to Generate Short URLs

You will now create an endpoint to generate short URLs. To do this, we will create logic to handle POST requests in handleRequest.

When a POST request is made to the worker, we will create a key-value entry in the namespace where the key will be the randomly generated string, while the value will be a destination URL contained in the request body.

Modify index.js to create a KV entry on POST requests:

index.js
addEventListener('fetch', event => {
 event.respondWith(handleRequest(event.request))
})
/**
* Respond with hello worker text
* @param {Request} request
*/
async function handleRequest(request) {

   if( request.method === "POST"){
       const neededURL = await  request.text()
       const rand = Math.random().toString(30).substr(2, 5);
       await URL_SPACE.put(rand, neededURL)
       return new Response(rand)
   }

 return new Response('Hi worker!', {
   headers: { 'content-type': 'text/plain' },
 })
}

When we make a POST request, this gets the URL that we want to redirect to from the request body by calling request.text. It then generates a pseudo-random string with Math.random().toString(30).substr(2, 5) and stores the key-value pair in our namespace with URL_SPACE.put(rand, neededURL).

Make a post request to locahost:8787 by running:

curl -d "https://www.google.com" -H "Content-Type: text/plain" -X POST http://localhost:8787/

You should receive the random string as the response.

Output
rrenb

Another thing you certainly want to do is to make the destination URL have the https protocol if no protocol is added to the URL.

For example, after make the following request

curl -d "www.google.com" -H "Content-Type: text/plain" -X POST http://localhost:8787/`

We want the destination to be stored as https://www.google.com instead of www.google.com.

Modify the handleRequest function:

index.js
async function handleRequest(request) {

    if( request.method === "POST"){
        const neededURL = await  request.text()
        let cleanUrl
        if(!neededURL.match(/http:\/\//g) && !neededURL.match(/http:\/\//g)){
            cleanUrl = "https://" + neededURL
        }
        else {
            cleanUrl = neededURL
        }
        const rand = Math.random().toString(30).substr(2, 5);
        await URL_SPACE.put(rand, cleanUrl)
        return new Response(rand)
    }

  return new Response('Hello worker!', {
    headers: { 'content-type': 'text/plain' },
  })
}

The extra addition does a regular expression search for http:// and https:// in the destination URL, and if neither is found, it appends https:// to the destination URL before storing it.

You have now successfully generated and stored the random strings together with the corresponding destination. In the next step, we will redirect short link visits to the intended destinations.

To achieve this aim, you will use a regular expression search on the request URL to strip off the https://.../ or http://.../ part of the URL and get the random string. You will then search for the entry in the URL_SPACE namespace, whose key is the random string, and redirect the user to that destination.

Edit the handleRequest function:

index.js
async function handleRequest(request) {

    if( request.method === "POST"){
        ...
    }

    if( request.method === "GET" ){
        let shortCode = request.url.replace(/https:\/\/.+?\//g, "")
        shortCode = shortCode.replace(/http:\/\/.+?\//g, "")

        if(shortCode !== "") {
            const redirectTo = await URL_SPACE.get(shortCode)
            return Response.redirect(redirectTo, 301)
        }
        else {
            return new Response( 'Welcome to shortlinker, make a post request to add a shortlink', {
                headers: { 'content-type': 'text/plain' },
            })
        }
    }

  return new Response('Hi worker!', {
    headers: { 'content-type': 'text/plain' },
  })
}

The new code addition intercepts only GET requests. It uses request.url.replace(/https:\/\/.+?\//g, "") to remove the protocol portion of the URL and assigns the random part left to the shortCode variable.

When we visit http://locahost:8787, the above addition will intercept the request, and the shortCode variable will be an empty string. We wouldn't want to redirect a user that visits the homepage. That is why there is a conditional statement to only redirect non-homepage visits. The worker then searches URL_SPACE for the destination that the value of shortCode is tied to and makes the redirect with Response.redirect().

To test this new addition, generate a random string that redirects to https://www.google.com with the following command:

curl -d "https://www.google.com" -H "Content-Type: text/plain" -X POST http://localhost:8787/

You will get a random string as the response.

Visit the short link with the following command, replacing short-string with the random string generated in the previous step

curl -v http://localhost:8787/short-string

You will have an output like the following. You will notice that a 301 redirect is made to https://www.google.com

Output
*   Trying ::1...
* TCP_NODELAY set
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8787 (#0)
> GET /short-string HTTP/1.1
> Host: localhost:8787
> User-Agent: curl/7.55.1
> Accept: */*
>...
< HTTP/1.1 301 Moved Permanently
< date: Fri, 23 Apr 2021 19:17:54 GMT
...
< location: https://www.google.com
...

In this step, you set up the redirects for the short links. In the next step, you will add a web interface at the homepage to create new shortlinks directly from a browser.

Step 7 — Adding a Web Interface

In this final step, you will add a simple user interface that will be used for generating short links.

Modify index.js:

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

const htmlBody = `
<html>
   <head>
       <style>
           #url {
               font-size: 2em;
               width: 100%;
               max-width: 500px;
           }
           #submit {
               font-size: 2em;
           }
       </style>
       <script>
           const submitURL = () => {
               document.getElementById("status").innerHTML="creating short url"
               //await call url for new shortlink and return
               fetch('/', {method: "POST", body: document.getElementById("url").value})
                   .then(data => data.text())
                   .then(data => {

                       console.log('ready')
                       document.getElementById("status").innerHTML="Your short URL: http://localhost:8787/" + data
                   } )
           }
       </script>
   </head>
   <body>
       <h1 id="title">URL Shortener</h1>
       <input type="text" id="url" placeholder="enter url here" />
       <button id="submit" onclick="submitURL()">Submit</button>
       <div id="status"></div>
   </body>
</html>
`

/**
* Respond with hello worker text
* @param {Request} request
*/
async function handleRequest(request) {
   if( request.method === "POST"){
       const neededURL = await  request.text()
       let cleanURL
       //add necessary protocols if needed to url
       if(!neededURL.match(/http:\/\//g) && !neededURL.match(/https:\/\//g)){
           cleanURL = "https://" + neededURL
       }
       else {
           cleanURL = neededURL
       }

       const rand = Math.random().toString(30).substr(2, 5);
       await URL_SPACE.put(rand, cleanURL)
       return new Response(rand)
   }

   if( request.method === "GET" ){
       let shortCode = request.url.replace(/https:\/\/.+?\//g, "")
       shortCode = shortCode.replace(/http:\/\/.+?\//g, "")

       if(shortCode !== "") {
           const redirectTo = await URL_SPACE.get(shortCode)
           return Response.redirect(redirectTo, 301)
       }
       else {
           return new Response( htmlBody, {
               headers: { 'content-type': 'text/html' },
           })
       }
   }

}

We created a htmlBody variable that contains the template for the home page's interface. We return this string when a visitor visits the homepage. Since we now have a webpage where we can create short links, visit http://localhost:8787 in a browser. Type in a destination you want to link to in the space provided and click submit. A short link will be generated and displayed. When you visit that short link, you will be redirected to the desired destination.

Conclusion

Now you have successfully created a URL shortener that runs on Cloudflare's edge network. You can read this section on deployment in the Workers documentation to learn how to deploy the production. Before finally deploying the URL shortener, you will have to make a slight modification to the template for the homepage.

Go to the following line in the htmlBody string in index.js

index.js
...
document.getElementById("status").innerHTML="Your short URL: http://localhost:8787/" + data
...

Replace http://locahost:8787/ with https://subdomain.workers.dev/ where subdomain is the workers.dev subdomain where your app will be deployed.

You can try a demo of this URL shortener here!