29
Build a Link Shortener with Cloudflare Workers: The back-end
In this part of the tutorial, we'll use Cloudflare Workers to accomplish two things and build the back-end of our app:
- Creating a new short link and returning the slug of that short link (e.g given a link like
shorturl.at/gIUX4
, the slug isgIUX4
) - Redirecting from a short link to the full link, given a slug
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
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.
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"
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" }
]
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:
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
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" }
]
Then my wrangler.toml
file would now have this under kv_namespaces
:
kv_namespaces = [
{ binding = "SHORTEN", id = "48ae98ff404a460a87d0348bb5197595", preview_id = "d7044b5c3dde494a9baf1d3803921f1c" }
]
If you check the Cloudflare Workers KV section of the dashboard, you should now see two namespaces, one appended with _preview
and one not.
We'll define an API endpoint, POST /links
, that takes in a JSON payload like { "url": "https://google.com" }
, then:
- Generates a random, alphanumeric slug using
nanoid
- 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 - 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,
);
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 });
}
});
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 });
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))
})
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))
})
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"}
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!
You can also remove the handleRequest
function the template comes with from index.js
.
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,
});
}
});
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
.
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.
29