Orange Book
End-to-end projects

Project nine, URL shortener

Build a minimal short link service with a Worker and KV, with authenticated creation, 302 redirects, expiry, and click stats.

Edited and verified by Orange Book Editorial Team ·

PROJECT 09BEGINNERabout 50 minutesOutcome: a short link service you can create, redirect, and measure

Completion criteria

An admin creates short links with a bearer token, using a custom slug or a random six-character slug and an optional expiry; visitors get a 302 redirect; click counts are readable through a stats endpoint; invalid URLs, duplicate slugs, and unauthorized requests all get stable error codes.

Prerequisites and companion example

You need Node.js 22, pnpm, and the repository dependencies. The example lives in examples/url-shortener and uses local KV emulation only; it touches no remote state. Copy the local secret and start the Worker:

cp examples/url-shortener/.dev.vars.example examples/url-shortener/.dev.vars
pnpm wrangler dev --config examples/url-shortener/wrangler.jsonc

In another terminal, run node --test examples/url-shortener/test/index.test.mjs; eight tests should pass. Then create a link with the curl commands in the example README; the response status should be 201, and curl -i http://127.0.0.1:8787/<slug> should return 302 with a Location header pointing at the target URL. For background on bindings and secrets, see Bindings and secrets.

Example authentication boundary

ADMIN_KEY only demonstrates server-to-server authentication. A public shortener must use a longer, regularly rotated key kept in a server-side secret, never in browser code or logs.

Design and data model

One Worker plus one KV namespace (binding name LINKS) is enough. The KV key is the slug, and the value is a JSON document: {url, clicks, createdAt, expiresAt}. Expiry uses the native KV expirationTtl, so expired keys disappear on their own with no scheduled job. The API surface has three endpoints:

EndpointAuthBehavior
POST /api/shortenBearer ADMIN_KEYCreate a link with a custom slug or a random six-character slug, optionally with ttlSeconds
GET /<slug>None302 redirect to the target URL, incrementing the click count
GET /api/stats/<slug>Bearer ADMIN_KEYReturn the target URL, click count, and creation and expiry timestamps

Slugs must match ^[a-zA-Z0-9_-]{1,64}$, and target URLs are limited to the http: and https: protocols, which keeps schemes such as javascript: from being weaponized through redirects.

Implementing the core endpoints

The create path authenticates first, then validates the URL, the TTL, and the slug in order; a duplicate slug returns a stable 409 slug_taken. Random slugs come from crypto.getRandomValues, with a retry on collision:

const SLUG_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;

function parseTargetUrl(value) {
  if (typeof value !== 'string') return null;
  try {
    const url = new URL(value);
    return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
  } catch {
    return null;
  }
}

function randomSlug(length = 6) {
  const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  const bytes = crypto.getRandomValues(new Uint8Array(length));
  return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');
}

The redirect path reads the JSON, increments clicks, writes it back, then answers 302. The write recomputes the remaining expirationTtl from expiresAt so a click does not erase the expiry. Remember that KV is eventually consistent: this read-modify-write counter can lose concurrent increments, and reads may lag behind edge caching, so the click count is an approximation suited to trend watching, not billing. Redirect responses carry cache-control: no-store so every visit passes through the Worker; for the full caching discussion, see Cache rules as the first policy.

KV or D1

Short links are a classic read-heavy workload, and both stores work. The trade-off is consistency and query power:

DimensionKVD1
Read latencyEdge-cached worldwide, very lowSingle-region primary, distant regions pay a round trip
ConsistencyEventual, writes propagate globally in up to about 60 secondsStrong
Click countingRead-modify-write, approximate under concurrencyUPDATE ... SET clicks = clicks + 1 is atomic and exact
ExpiryNative expirationTtlNeeds a scheduled job or query-time filtering
ReportingKey reads onlyArbitrary SQL aggregation

Pick KV when you want fast redirects, a simple shape, and approximate counts; pick D1 when you need exact counts, rich reports, or relational data. For a systematic chooser, see Storage chooser.

Abuse prevention and security boundaries

A public creation endpoint will be abused: put Turnstile in front of the human-facing creation flow, add WAF rate limiting per IP, and treat both as complements to the server-side ADMIN_KEY guard, not replacements. Configuration details are in Turnstile and WAF. Also reserve the /api/ prefix for system routes before user slugs are matched, so nobody can register a slug that hijacks an API path.

Mature projects to study

Short links have been built repeatedly in the Cloudflare ecosystem; read these before writing your own:

  • miantiao-me/Sink (7,000+ stars): a Nuxt 3 full-stack shortener with links in KV and visit analytics in Analytics Engine, supporting custom slugs, UTM parameters, expiry, access passwords, and QR codes, under AGPL-3.0; the project migrated from ccbikai/Sink.
  • xyTom/Url-Shorten-Worker (1,700+ stars): a single-file Worker plus KV, small enough to read end to end; it added captcha-based abuse protection in 2025.
  • x-dr/short (400+ stars): a Chinese-language project built on Pages Functions plus D1, a useful counterpoint to the KV design in this guide.

Verification and troubleshooting

SymptomCheck firstRecovery
401 unauthorized.dev.vars and the Authorization: Bearer header matchRecopy the local template; never commit the value
400 invalid_urlTarget starts with http:// or https://Fix the request body; other protocols are unsupported
409 slug_takenSlug already existsChoose another slug or delete the old key
Redirects work but the count stallsKV eventual-consistency lag or concurrent overwritesWait up to about 60 seconds and recheck; move to D1 for exact counts
Expired key still redirects brieflyStale value in the edge cacheExpected behavior; filter by expiry time in reports

Remote boundaries and rollback

Creating the remote KV namespace, writing production secrets, and deploying the Worker all modify Cloudflare state, and this tutorial does not automate them. Rollback of a deployment does not delete link data already written to KV, nor does it restore a deleted namespace: export or record the critical slug mappings before rolling back, verify that core short links still redirect afterwards, and only delete or recreate a namespace once you have confirmed no production traffic depends on it.

Next: Set cache rules as the first policy for redirects and static assets.

Official sources

Did this page help you complete your goal?

Beta feedback is generated in this browser and is never uploaded automatically.

On this page