Create URL shortener by Cloudflare Workers
Published: 2023-07-28 11:33:50

# Introduction

In order to use a domain name which was unused, I developed a web service of URL shortener by myself.

## Choosing Cloud Providers and its Services

The service candidates for use include container-based service or serverless computing services. URL shortener service has a simple processing logic and doesn't require storing data as in-memory data. Therefore, serverless computing services are chosen.

I have experience for using public cloud services such as AWS, GCP, and Azure. In contrast, I have no experience for using the web services which is provided by Cloudflare. Therefore, this project uses [Cloudflare Workers](https://workers.cloudflare.com/) as the service platform.
The requirement for this project is to store URL on databases. Cloudflare provides Cloudflare Workers KV as key-value pair store. These services are provided as a free tier service.

## System Architecture

```
      Clients
         |
         v
+-----------------+     +--------------------+
|Cloudflare Worker| --> |Cloudflare Worker KV|
+-----------------+     +--------------------+
```

## Codes

Following codes are deployed on Cloudflare Workers as a function. Then, you create a key-value store on Cloudflare Workers KV, and this is named as `MY_KV`.

```javascript
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  if (request.method === "GET") {
    return readRedirect(request)
  } else if (request.method === "POST") {
    return createRedirect(request)
  } else {
    return new Response("Forbidden", { status: 403 })
  }
};

async function readRedirect(request) {
  const url = new URL(request.url);
  const req_path = url.pathname;
  
  const value = await MY_KV.get(req_path);
  if (value === null) {
    console.log("Not Found", req_path);
    return new Response("Not Found", { status: 404 });
  }

  console.log("Redirect", req_path);
  const statusCode = 301;
  return Response.redirect(value, statusCode);
};

async function createRedirect(request) {
  const url = new URL(request.url);
  const req_path = url.pathname;

  const secret_token = "xxxxx-xxxxxx-xxxxxx-xxxxxx";
  if (req_path !== "/token/" + secret_token) {
    return new Response("Invalid Token", { status: 401 });
  }

  const content_type = request.headers.get("content-type");
  if (!content_type.includes("application/json")) {
    return new Response("Invalid Content Type", { status: 400 });
  }
  const request_body = await request.json();
  const redirect_from_path = request_body["from_path"];  // "/foo"
  const redirect_to_url = request_body["to_url"]; // http://example.com
  await MY_KV.put(redirect_from_path, redirect_to_url);
  return new Response("https://1ch.dev" + redirect_from_path);
};
```

## Usage

### iOS Shortcuts

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqjb5wy01lc3m7lxb9sm.PNG)

### CLI commands

This code is supported for `.zshrc`.

```bash
function tiny() {
    read to_url
    echo $to_url | grep 'http\(s\)\?://' || {
        echo "Invalid URL"
        return 1
    }

    from_path="/cli-$(date +%s)"
    curl -XPOST -H 'Content-Type: application/json' \
      -d "{\"from_path\": \"$from_path\", \"to_url\": \"$to_url\"}" \
      https://1ch-dev.xxxxxxxx.workers.dev/token/xxxxxxxx \
      | pbcopy
}
```

Usage:

```
echo "http://example.com/" | tiny
```