visit
Given a URL, our service should return a unique and short URL of it. E.g. //jerrynsh.com/how-to-write-clean-code-in-python/
→ s.jerrynsh.com/UcFDnviQ
Whenever a user tries to access s.jerrynsh.com/UcFDnviQ
, the user would be directed back to the original URL.
For our POC, the key of our KV would be a UUID that follows after our domain name (e.g. s.jerrynsh.com/UcFDnviQ
) while the value would consist of the long URL given by the users.
# Production namespace:
wrangler kv:namespace create "URL_DB"
# This namespace is used for `wrangler dev` local testing:
wrangler kv:namespace create "URL_DB" --preview
For creating these KV namespaces, we also need to update our wrangler.toml
file to include the namespace bindings accordingly. You can view your KV’s dashboard by visiting //dash.cloudflare.com/<your_cloudflare_account_id>/workers/kv/namespaces
.
//jerrynsh.com/stop-using-exceptions-like-this-in-python/ → UUID Generator → s5OwuhqW → s.jerrynsh.com/s5OwuhqW
//betterprogramming.pub/how-to-write-clean-code-in-python-5d67746133f2 → MD5 Hash → 99d641e9923e135bd5f3a19dca1afbfa → 99d641e9 → s.jerrynsh.com/99d641e9
//betterprogramming.pub/3-useful-python-f-string-tricks-you-probably-dont-know-f908f7ed6cf5 → Counter → s.jerrynsh.com/12345678
Nonetheless, I do not want users to be able to randomly guess a short URL by simply visiting s.jerrynsh.com/12345678
. So, this solution is out of the question.
For our POC, we are going with solution 1 as it is straightforward to implement and I am fine with duplicates. To cope with duplicates, we could cache our users’ requests to shorten URLs.
To generate a UUID, we are using the nanoid
package. To estimate our rate of collision, we can use the :
Okay enough talk, let’s write some code! To handle the possibility of collision, we simply have to keep retrying:
// utils/urlKey.js
import { customAlphabet } from "nanoid";
const ALPHABET =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/*
Generate a unique `urlKey` using `nanoid` package.
Keep retrying until a unique urlKey which does not exist in the URL_DB.
*/
export const generateUniqueUrlKey = async () => {
const nanoId = customAlphabet(ALPHABET, 8);
let urlKey = nanoId();
while ((await URL_DB.get(urlKey)) !== null) {
urlKey = nanoId();
}
return urlKey;
};
In this section, we will define the API endpoints that we would like to support. This project is initialized using the itty-router
worker — it helps us with all the routing logic:
wrangler generate <project-name> //github.com/cloudflare/worker-template-router
// index.js
import { Router } from "itty-router";
import { createShortUrl } from "./src/handlers/createShortUrl";
import { redirectShortUrl } from "./src/handlers/redirectShortUrl";
import { LANDING_PAGE_HTML } from "./src/utils/constants";
const router = Router();
// GET landing page html
router.get("/", () => {
return new Response(LANDING_PAGE_HTML, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
});
});
// GET redirects short URL to its original URL.
router.get("/:text", redirectShortUrl);
// POST creates a short URL that is associated with its an original URL.
router.post("/api/url", createShortUrl);
// 404 for everything else.
router.all("*", () => new Response("Not Found", { status: 404 }));
// All incoming requests are passed to the router where your routes are called and the response is sent.
addEventListener("fetch", (e) => {
e.respondWith(router.handle(e.request));
});
In the name of a better user experience, I have created a simple HTML landing page that anyone could use; you can get the landing page’s HTML .
To start, we need a POST endpoint (/api/url
) that calls createShortUrl
that parses the originalUrl
from the body and generates a short URL from it.
// handlers/createShortUrl.js
import { generateUniqueUrlKey } from "../utils/urlKey";
export const createShortUrl = async (request, event) => {
try {
const urlKey = await generateUniqueUrlKey();
const { host } = new URL(request.url);
const shortUrl = `//${host}/${urlKey}`;
const { originalUrl } = await request.json();
const response = new Response(
JSON.stringify({
urlKey,
shortUrl,
originalUrl,
}),
{ headers: { "Content-Type": "application/json" } },
);
event.waitUntil(URL_DB.put(urlKey, originalUrl));
return response;
} catch (error) {
console.error(error, error.stack);
return new Response("Unexpected Error", { status: 500 });
}
};
To try this out locally (you can use wrangler dev
to start the server locally), use the curl
command bellow:
curl --request POST \\
--url //127.0.0.1:8787/api/url \\
--header 'Content-Type: application/json' \\
--data '{
"originalUrl": "//www.google.com/"
}'
// handlers/redirectShortUrl.js
export const redirectShortUrl = async ({ params }) => {
const urlKey = decodeURIComponent(params.text);
const originalUrl = await URL_DB.get(urlKey);
if (originalUrl) {
return Response.redirect(originalUrl, 301);
}
return new Response("Invalid Short URL", { status: 404 });
};
How about deletion? Since the user does not require any authorization to shorten any URL, the decision was made to move forward without a deletion API as it makes no sense that any user can simply delete another user’s short URL.
NOTE: Currently, this only works with a custom domain.What happens if a user decides to repeatedly shorten the same URL? We wouldn’t want our KV to end up with duplicated URLs with unique UUID assigned to them right?
import { URL_CACHE } from "../utils/constants";
export const shortUrlCacheMiddleware = async (request) => {
const { originalUrl } = await request.clone().json();
if (!originalUrl) {
return new Response("Invalid Request Body", {
status: 400,
});
}
const cache = await caches.open(URL_CACHE);
const response = await cache.match(originalUrl);
if (response) {
console.log("Serving response from cache.");
return response;
}
};
To use this cache middleware, simply update our index.js
accordingly:
// index.js
...
router.post('/api/url', shortUrlCacheMiddleware, createShortUrl)
...
// handlers/createShortUrl.js
import { URL_CACHE } from "../utils/constants";
import { generateUniqueUrlKey } from "../utils/urlKey";
export const createShortUrl = async (request, event) => {
try {
const urlKey = await generateUniqueUrlKey();
const { host } = new URL(request.url);
const shortUrl = `//${host}/${urlKey}`;
const { originalUrl } = await request.json();
const response = new Response(
JSON.stringify({
urlKey,
shortUrl,
originalUrl,
}),
{ headers: { "Content-Type": "application/json" } },
);
const cache = await caches.open(URL_CACHE); // Access our API cache instance
event.waitUntil(URL_DB.put(urlKey, originalUrl));
event.waitUntil(cache.put(originalUrl, response.clone())); // Update our cache here
return response;
} catch (error) {
console.error(error, error.stack);
return new Response("Unexpected Error", { status: 500 });
}
};
During my testing with wrangler dev
, it seems like the .
The workaround to test this is to run wrangler publish
to publish the application on a custom domain. You can validate the changes by sending a request to the /api/url
endpoint while observing the log via wrangler tail
.
Before publishing your code you need to edit the wrangler.toml
file and add your Cloudflare account_id
inside. You can read more information about configuring and publishing your code can be found in the .
To deploy and publish any new changes to your Cloudflare Worker, simply run wrangler publish
. To deploy your application to a custom domain, check out this .