sellIntegration
link

Blueprint: a URL shortener (your first full-stack app)

Paste a long link, get a short one back, and click it to jump — the perfect first project.

3Layers wired
6Short code length
302Redirect status
1KV namespace
insights

What are we building?

A URL shortener is the classic 'hello world' of full-stack apps. You paste a long, ugly link, the app stores it under a short random code, and visiting that code redirects you to the original link. With Cloudflare you can build the whole thing with one HTML page, one Worker, and one KV namespace — no server to manage.

There are exactly two things the app does: (1) create — turn a long URL into a short code and save it; (2) resolve — take a short code and send the browser to the long URL. Let's see both as a picture.

sticky_note_2

Think of it like…

A coat-check ticket. You hand over a bulky coat (the long URL) and get a tiny numbered ticket (the short code). Later you show the ticket and the attendant brings back exactly your coat. KV is the rack of coats; the code is your ticket number.

schemaThe two flows at a glance

Open a short link

Create a short link

POST long URL

return short URL

302 redirect

put(code, url)

get(code)

KV store

Browser form

Worker create

Browser /code

Worker lookup

account_tree

How the pieces fit

Three small layers, each with one job. Requests flow front-end → Worker → KV and back. Because the Worker also serves the static form, your whole app lives at a single URL.

web

1. Front-end form

A plain HTML page with one input and a fetch() call. No framework needed.

bolt

2. Worker (the brain)

One function that reads the request, decides create vs resolve, and talks to KV.

key

3. KV (the memory)

A global key-value store holding every code → URL pair, read back in milliseconds.

tag

The short code

A tiny random string like abc123 that becomes the key and the last part of the short URL.

schemaInside the Worker: one router, two jobs

POST /api/shorten

GET /code

put(code, url)

get(code)

Incoming request

Match route

Create handler

Lookup handler

KV: LINKS

Return JSON short URL

Return 302 redirect

swap_vert

What happens on each click

Both flows talk to the same three players: your Browser, the Worker, and KV. Here is the exact back-and-forth for creating a link and then opening it.

schemaCreate, then resolve
KVWorkerBrowserKVWorkerBrowserCreate a short linkOpen the short linkPOST /api/shorten with long URLgenerate random codeput(code, longUrl)storedshort URL /abc123GET /abc123get(abc123)long URL302 redirect to long URL
construction

Build it step by step

Five steps from empty folder to a live link shortener. Create a project with npm create cloudflare@latest, then follow along — every file you need is below.

  1. Create a KV namespace

    A namespace is one isolated bucket of key-value pairs. Wrangler prints an id — copy it for the next step.

    bash
    npx wrangler kv namespace create LINKS
  2. Bind KV in wrangler.jsonc

    This config file names your entry Worker, points at the static files, and makes the namespace available in code as env.LINKS.

    jsonc
    {
      "name": "url-shortener",
      "main": "src/index.js",
      "compatibility_date": "2025-06-01",
      // Serve everything in ./public as static files (the HTML form lives here)
      "assets": { "directory": "./public" },
      // Make the KV namespace available in code as env.LINKS
      "kv_namespaces": [
        { "binding": "LINKS", "id": "<paste-your-id-here>" }
      ]
    }
  3. Build the front-end form

    Save this as public/index.html. It is served automatically at / and posts the URL to your Worker with fetch().

    html
    <!DOCTYPE html>
    <html lang="zh-Hant">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>URL Shortener 短網址</title>
      </head>
      <body>
        <h1>Shorten a URL 縮短網址</h1>
    
        <form id="shorten-form">
          <input
            id="long-url"
            type="url"
            placeholder="https://example.com/a/very/long/path"
            required
          />
          <button type="submit">Shorten</button>
        </form>
    
        <p id="result"></p>
    
        <script>
          const form = document.getElementById("shorten-form");
    
          form.addEventListener("submit", async (event) => {
            event.preventDefault();
            const longUrl = document.getElementById("long-url").value;
    
            // POST the long URL to our Worker as JSON
            const response = await fetch("/api/shorten", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ url: longUrl }),
            });
    
            const data = await response.json();
            const result = document.getElementById("result");
    
            if (data.short) {
              const shortUrl = location.origin + data.short;
              result.innerHTML =
                'Short link: <a href="' + shortUrl + '">' + shortUrl + "</a>";
            } else {
              result.textContent = data.error || "Something went wrong";
            }
          });
        </script>
      </body>
    </html>
  4. Write the Worker

    Save this as src/index.js. On POST it generates a code and saves it; on GET it looks the code up and redirects. This is the entire back-end.

    js
    // src/index.js — the entire back-end of the URL shortener
    
    // Build a random short code, e.g. "a8Kp2Z", from safe characters.
    function makeCode(length = 6) {
      const chars =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
      const bytes = crypto.getRandomValues(new Uint8Array(length));
      let code = "";
      for (const byte of bytes) {
        code += chars[byte % chars.length];
      }
      return code;
    }
    
    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
    
        // 1) CREATE a short link:  POST /api/shorten  with { "url": "https://..." }
        if (request.method === "POST" && url.pathname === "/api/shorten") {
          const body = await request.json();
          const longUrl = body.url;
    
          // Validate the input before trusting it.
          const ok =
            longUrl &&
            (longUrl.startsWith("http://") || longUrl.startsWith("https://"));
          if (!ok) {
            return Response.json(
              { error: "Please send a valid http(s) URL" },
              { status: 400 }
            );
          }
    
          const code = makeCode(6);
          await env.LINKS.put(code, longUrl); // save the pair: code -> long URL
          return Response.json({ short: "/" + code });
        }
    
        // 2) RESOLVE a short link:  GET /<code>  ->  302 redirect to the long URL
        if (request.method === "GET") {
          const code = url.pathname.slice(1); // drop the leading "/"
          const longUrl = await env.LINKS.get(code); // look the code up in KV
          if (longUrl) {
            return Response.redirect(longUrl, 302);
          }
          return new Response("Short link not found", { status: 404 });
        }
    
        return new Response("Method not allowed", { status: 405 });
      },
    };
  5. Run locally, then deploy

    wrangler dev runs it on your machine; wrangler deploy puts it on Cloudflare's global network with a public URL.

    bash
    # Run it on your own machine first
    npx wrangler dev
    # open http://localhost:8787 , paste a URL, then click the short link
    
    # Happy with it? Ship it worldwide:
    npx wrangler deploy
school

Key concepts

key

KV stores code → URL

Each short code is a key; the long URL is its value. put(code, url) saves the pair, get(code) reads it back. No tables, no SQL.

subdirectory_arrow_right

302 redirect

Response.redirect(url, 302) tells the browser 'this link lives somewhere else, go there now'. 302 = a temporary redirect, so you can change the target later.

casino

Random short code

We pick 6 characters at random from a-z, A-Z, 0-9 — over 56 billion combinations, so collisions are extremely unlikely for a small app.

schedule

Eventual consistency is fine here

A freshly created code can take up to ~60s to appear in every region. For sharing links that is perfectly acceptable.

web

Static assets + Worker

The HTML form is a static file served automatically; any path that is not a file (like /abc123) falls through to your Worker.

shield

Validate the input

Always check the submitted URL really starts with http:// or https:// before saving it — never trust raw form data.

info

301 vs 302

301 = permanent (browsers and search engines cache it hard); 302 = temporary. Start with 302 while learning so you can freely change where a code points.

trending_up

Level it up: count the clicks

Once the basics work, a fun next step is counting how many times each short link is opened. The quick way: keep a counter in KV. The robust way: log each click into a D1 (SQL) table so you can run analytics.

Quick way — a KV counter

jsadd inside the GET branch
// Inside the GET branch, right after you read longUrl:
const key = "clicks:" + code;
const hits = parseInt((await env.LINKS.get(key)) || "0", 10);
await env.LINKS.put(key, String(hits + 1)); // best-effort click count

Robust way — a D1 table

With D1 you keep one row per link and one row per click. The shape looks like this:

schemaClick-tracking schema (D1 upgrade)

records

LINKS

text

code

PK

text

long_url

int

created_at

CLICKS

int

id

PK

text

code

FK

int

clicked_at

sqlschema.sql
-- schema.sql  (run with: npx wrangler d1 execute mydb --file schema.sql)
CREATE TABLE links (
  code       TEXT PRIMARY KEY,
  long_url   TEXT NOT NULL,
  created_at INTEGER NOT NULL
);

CREATE TABLE clicks (
  id         INTEGER PRIMARY KEY,
  code       TEXT NOT NULL,
  clicked_at INTEGER NOT NULL
);
lightbulb

KV or D1?

KV is perfect for the code → URL lookup (tons of fast reads). D1 is better for click logs you want to query and aggregate. Many real apps use both together.

tips_and_updates

Tips, traps & limits

warning

Open redirects can be abused

Because anyone can shorten any URL, bad actors could hide phishing links behind your domain. For a learning project it's fine; for production, consider a blocklist or only allowing your own URLs.

  • KV free tier: 100,000 reads/day and 1,000 writes/day — plenty for a personal shortener.
  • Reads are fast and cheap; writes are limited to ~1 per second per key, which is fine since each code is written once.
  • Check for an existing code before saving if you want to be extra safe against the tiny collision chance.
  • A new code may take up to ~60 seconds to be readable in every region (eventual consistency).