DEV Community

loading...

Build a Link Shortener with Cloudflare Workers: The back-end

Matthew Mascioni
Web developer in Toronto 🇨🇦. Passionate about Python, APIs, docs, running and awful puns.
・6 min read

In this part of the tutorial, we'll use Cloudflare Workers to accomplish two things and build the back-end of our app:

  1. Creating a new short link and returning the slug of that short link (e.g given a link like shorturl.at/gIUX4, the slug is gIUX4)
  2. Redirecting from a short link to the full link, given a slug

Before we begin

If you haven't gone through the introduction to the project part of this tutorial yet to set up your environment, head back to that part first!

Let's install two packages this project will depend on. We'll make use of these two very lightweight packages:

  • itty-router: Will allow us to declare URL routes and parameters in those routes easily
  • nanoid: What we'll use to generate the random slugs to access the URLs at

Switch to the project directory we created in the last part of this tutorial, and use npm to install itty-router and nanoid:

npm i itty-router nanoid
Enter fullscreen mode Exit fullscreen mode

Also, please check to ensure that you've populated your Account ID in your wrangler.toml file, and have set the type for your project to webpack to package up these dependencies.

Setting up our Workers KV Store

To store slugs and the URLs they point to, we'll use Workers KV. It's optimized for high-read applications where we'll need to access these keys frequently, and can be easily accessed from your Worker code. Each key will be a slug (e.g gIUX4), and the value the URL they point to (e.g https://www.youtube.com/watch?v=dQw4w9WgXcQ).

KV stores are organized in namespaces, and you can create them using Wrangler. Let's create one called SHORTEN:

$ wrangler kv:namespace create "SHORTEN"
Enter fullscreen mode Exit fullscreen mode

You should get some output back from Wrangler on what to add to your wrangler.toml file:

🌀  Creating namespace with title "rd-test-SHORTEN"
✨  Success!
Add the following to your configuration file:
kv_namespaces = [ 
         { binding = "SHORTEN", id = "48ae98ff404a460a87d0348bb5197595" }
]
Enter fullscreen mode Exit fullscreen mode

In my case, I'd add that kv_namespaces entry to the end of my wrangler.toml file. The binding key here lets you access the KV namespace by the SHORTEN variable in your Worker code. If you log in to the Cloudflare Workers dashboard, you should be able to see your namespace listed under KV too:

The namespace listed on the Cloudflare dashboard

Clicking "View" lets you see all associated keys and values, though it should be empty now. In your Worker's code, you can now interface with this namespace through the SHORTEN variable, which we'll see in a moment.

Finally, create a preview namespace. This will be automatically used in development (i.e. when running wrangler dev) when previewing your Worker:

$ wrangler kv:namespace create "SHORTEN" --preview
Enter fullscreen mode Exit fullscreen mode

Take the preview_id provided in the output of this command and add it to the wrangler.toml file, in the same entry as your SHORTEN KV namespace. For example, if my output was this:

🌀  Creating namespace with title "rd-test-SHORTEN_preview"
✨  Success!
Add the following to your configuration file:
kv_namespaces = [ 
         { binding = "SHORTEN", preview_id = "d7044b5c3dde494a9baf1d3803921f1c" }
]
Enter fullscreen mode Exit fullscreen mode

Then my wrangler.toml file would now have this under kv_namespaces:

kv_namespaces = [ 
    { binding = "SHORTEN", id = "48ae98ff404a460a87d0348bb5197595", preview_id = "d7044b5c3dde494a9baf1d3803921f1c" }
]
Enter fullscreen mode Exit fullscreen mode

If you check the Cloudflare Workers KV section of the dashboard, you should now see two namespaces, one appended with _preview and one not.

Generating slugs and creating short links

We'll define an API endpoint, POST /links, that takes in a JSON payload like { "url": "https://google.com" }, then:

  1. Generates a random, alphanumeric slug using nanoid
  2. Saves a new entry to the SHORTEN KV namespace with the key as the slug in (1) and the value as the URL in the request payload
  3. Returns the slug, and the short URL

To do this, open up index.js in your project folder. Let's import some functions we'll need from itty-router and nanoid, create a router, and set up a custom alphabet to pick our URL slugs from (the 6 ensures they're 6 characters):

import { Router } from 'itty-router';
import { customAlphabet } from 'nanoid';

const router = Router();
const nanoid = customAlphabet(
  '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
  6,
);
Enter fullscreen mode Exit fullscreen mode

Now, let's define our API endpoint and walk through what it's doing:

router.post('/links', async request => {
  let slug = nanoid();
  let requestBody = await request.json();
  if ('url' in requestBody) {
    // Add slug to our KV store so it can be retrieved later:
    await SHORTEN.put(slug, requestBody.url, { expirationTtl: 86400 });
    let shortenedURL = `${new URL(request.url).origin}/${slug}`;
    let responseBody = {
      message: 'Link shortened successfully',
      slug,
      shortened: shortenedURL,
    };
    return new Response(JSON.stringify(responseBody), {
      headers: { 'content-type': 'application/json' },
      status: 200,
    });
  } else {
    return new Response("Must provide a valid URL", { status: 400 });
  }
});
Enter fullscreen mode Exit fullscreen mode

First, we're registering a route at /links to accept POST requests, and consuming the request object passed from our fetch event the worker listens for. We're using our custom alphabet nanoid to generate a 6-character random slug, and then pulling the URL out of the request body. We also add this to our KV namespace we generated earlier:

await SHORTEN.put(slug, requestBody.url, { expirationTtl: 86400 });
Enter fullscreen mode Exit fullscreen mode

Notice the SHORTEN variable is bound to your Worker after adding it in wrangler.toml. Here we're setting the key to automatically expire in a day, but you can choose to set this to any value you want (or not make it expire at all!) If all goes well, we'll return the slug and the full shortened URL so the front-end can make use of it.

Go to this part of index.js that came with the template Wrangler used to create the project:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})
Enter fullscreen mode Exit fullscreen mode

This fetch event is what's received once an HTTP request is made to your Worker. We'll modify this handler to connect to your router function instead:

addEventListener('fetch', event => {
  event.respondWith(router.handle(event.request))
})
Enter fullscreen mode Exit fullscreen mode

In this way, your router will be passed the Request object in the fetch event and can act on it, passing the request off to the correct route (like the one we defined above). Let's test it out! Run wrangler dev and make a test request to your Worker:

$ curl -X "POST" "http://127.0.0.1:8787/links" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}'

{"message":"Link shortened successfully","slug":"TCqff7","shortened":"https://redirect.mascioni.workers.dev/TCqff7"}
Enter fullscreen mode Exit fullscreen mode

Note that the value in shortened depends on your Workers subdomain and project name you chose in the beginning. If you head over to your SHORTEN_preview KV namespace in the Workers dashboard, you should see the entry was added too!

The TCqff7 key now has a value for a link to everyone's favourite video

You can also remove the handleRequest function the template comes with from index.js.

Redirecting from short links to full links

The implementation for this is similar to the one for generating the short link, except this time we're calling .get on our KV namespace and returning a redirect.

Let's register another route on our router, in index.js:

router.get('/:slug', async request => {
  let link = await SHORTEN.get(request.params.slug);

  if (link) {
    return new Response(null, {
      headers: { Location: link },
      status: 301,
    });
  } else {
    return new Response('Key not found', {
      status: 404,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

This time, we're registering a GET /:slug route, and making use of the slug parameter to fetch a value from our KV namespace. If a value exists, we'll return it with a 301 status code and set the Location header appropriately to do the redirect, otherwise we'll throw a 404 error.

If wrangler dev isn't running anymore, run it again, and now make a GET request from your browser to try and retrieve the URL you stored earlier! In my case, my Worker is listening for requests on port 8787, and I saved a URL with slug TCqff7, so I'd go to http://127.0.0.1:8787/TCqff7.

🎉 Back-end is finished!

With that, the back-end of our app is basically done! We can now generate short URLs and redirect from them easily. In the next part of this tutorial, we'll use Workers Sites to build a front-end on top of this.

➡️ Onward to building the front-end!

Discussion (2)

Collapse
kevinrwhitley profile image
Kevin R. Whitley

Great article! As the author of itty-router, I’d like to mention it pairs best with some of the utility functions from itty-router-extras (I always use them together) to further shorten your code (errors, status codes, and responses). That said, it never hurts to show building Responses manually like you’ve done :)

npmjs.com/package/itty-router-extras

Collapse
mmascioni profile image
Matthew Mascioni Author • Edited

Thanks so much, glad you liked it! itty-router is an awesome package and I can't wait to try out itty-router-extras with it in my next project :)