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 ·
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.jsoncIn 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:
| Endpoint | Auth | Behavior |
|---|---|---|
POST /api/shorten | Bearer ADMIN_KEY | Create a link with a custom slug or a random six-character slug, optionally with ttlSeconds |
GET /<slug> | None | 302 redirect to the target URL, incrementing the click count |
GET /api/stats/<slug> | Bearer ADMIN_KEY | Return 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:
| Dimension | KV | D1 |
|---|---|---|
| Read latency | Edge-cached worldwide, very low | Single-region primary, distant regions pay a round trip |
| Consistency | Eventual, writes propagate globally in up to about 60 seconds | Strong |
| Click counting | Read-modify-write, approximate under concurrency | UPDATE ... SET clicks = clicks + 1 is atomic and exact |
| Expiry | Native expirationTtl | Needs a scheduled job or query-time filtering |
| Reporting | Key reads only | Arbitrary 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
| Symptom | Check first | Recovery |
|---|---|---|
401 unauthorized | .dev.vars and the Authorization: Bearer header match | Recopy the local template; never commit the value |
400 invalid_url | Target starts with http:// or https:// | Fix the request body; other protocols are unsupported |
409 slug_taken | Slug already exists | Choose another slug or delete the old key |
| Redirects work but the count stalls | KV eventual-consistency lag or concurrent overwrites | Wait up to about 60 seconds and recheck; move to D1 for exact counts |
| Expired key still redirects briefly | Stale value in the edge cache | Expected 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.
Project eight, a navigation site
Render a link directory with one Worker, store links in KV, cache the page with the Cache API, and purge that cache after authenticated writes.
Project ten, an app dashboard (Heimdall-style)
Build a minimal, controllable app dashboard with a Worker rendering tiles, KV holding the manifest, and parallel subrequest health checks.