beginner one sitting analytics
Build your own link shortener (a personal Bitly)
You will build a link shortener on Cloudflare Workers: a small function that maps short slugs (the memorable tail of a short URL, like go.you.com/xk29fz) to long destinations, sends visitors there with a 302 (a standard response meaning 'moved temporarily, go look over there'), writes one row per click into a SQLite database, and renders a scannable QR code for any link. The whole thing fits in one sitting on Cloudflare's free tier. What you are not rebuilding is Bitly's real product: a fifteen-year-old promise that a link printed on packaging still resolves today, kept by a company whose job is to keep it that way.
What you'll learn
- Deploy a serverless function (code that runs on demand at Cloudflare's edge, with no server to manage) using wrangler
- Model a key-value lookup in Workers KV and write event rows to a D1 SQLite database
- Redirect traffic with proper 302 responses while keeping logging off the critical path using ctx.waitUntil
- Read request facts like country and device type directly from Cloudflare's request.cf object
- Generate and serve QR code images from a single library call
Before you start
- A Cloudflare account (the free tier is enough)
- A domain you control, with access to its DNS settings
- Node.js 18 or newer installed
- wrangler CLI installed with npm install -g wrangler, then authenticated with wrangler login
The build
Create a Cloudflare Worker project with its KV namespace (a key-value store, read fast at the edge) and D1 database (Cloudflare's serverless SQLite) wired up, then deploy a health-check route. Nothing here involves design decisions; it is exact configuration that must be right before anything else runs. Verify the deployed URL responds before moving on.
step prompt
Set up a Cloudflare Worker project named linkbox. Requirements:
- Initialize wrangler v3 or newer in a folder called linkbox, with src/index.ts exporting a fetch handler.
- Create a KV namespace bound as LINKS and a D1 database bound as CLICKS, both declared in wrangler.toml.
- Add a GET /health route returning JSON {"ok":true} plus the current row count from CLICKS (expect 0).
- Apply a local D1 migration file migrations/0001_init.sql that creates an empty clicks table so later inserts succeed.
- Run wrangler deploy, then curl the printed workers.dev URL and confirm the health response.
- Keep all secrets out of wrangler.toml; anything sensitive goes through wrangler secret put later.
- Out of scope: no redirect logic, no tests, no CI.
- Warning: if d1 create complains about a missing location hint, pass --location and pin the same region as your KV namespace.
This is the heart of the lesson: a create endpoint that stores slug-to-URL pairs, a catch-all route that looks up the slug and answers with a 302, and a click logger writing to D1 inside ctx.waitUntil (which lets the response leave immediately while background work finishes). You will also serve a QR image per link and a small stats query. Drive your assistant through this one piece at a time, testing each route with curl before adding the next.
step prompt
Add the redirect engine to the existing linkbox worker, using the LINKS KV namespace and the CLICKS D1 database from the scaffold. Requirements:
- POST /create accepts JSON {"url": "...", "backHalf": "optional"}; generate a random 6-character base62 slug when backHalf is absent, store it in LINKS under the key link:<slug>, and return HTTP 409 on a duplicate slug instead of overwriting it.
- GET /<slug> reads link:<slug> from LINKS and responds 302 with a Location header; unknown slugs get a plain-text 404.
- Insert one row per click into the clicks table (columns: slug, ts, country, city, device, referer) inside ctx.waitUntil so the redirect never waits on the write; take country and city from request.cf and derive device from a simple user-agent regex (mobile, tablet, desktop).
- Serve PNG QR codes at /qr/<slug> with the qrcode npm package encoding the public short URL, and set Cache-Control so repeat scans come from cache.
- Add GET /stats/<slug> returning JSON with total clicks plus counts grouped by country and by device.
- Out of scope: no authentication, no rate limiting, no bulk import.
- Warning: request.cf fields are undefined on localhost, so stub them with a fixture object during wrangler dev.
Open the Cloudflare dashboard, add a custom domain such as go.yourdomain.com to the linkbox worker, and let Cloudflare create the DNS record for it. Then create a test link and visit go.yourdomain.com/<slug> from your phone to watch the redirect fire against real traffic. This step is yours alone because DNS and routing sit behind account ownership, exactly the kind of surface an assistant should never touch on your behalf.
What you won't get
- Links resolve for exactly as long as you keep renewing your domain; the lifetime is yours to manage
- Short URLs carry your own domain name rather than a widely recognized brand, which some spam filters treat differently
- Scope covers redirects, click rows, and generated QR images; campaign grouping, landing pages, and a managed QR dashboard stay out
- Analytics arrive as raw per-click rows; country and device summaries are queries you write yourself
- Questions get answered by documentation and community forums; there is no support desk or uptime agreement
Why people still pay — and what that teaches you
brand-trust: Bitly wins because bit.ly is a promise made by an institution: a link on a 2019 brochure still resolving feels safe, and teams pay to avoid betting print budgets on a hobby project. The builder's takeaway is that your domain is your own version of that promise, so pick one you intend to keep, and notice that name recognition cuts both ways now that generic shorteners land in spam folders.
scale-infra: Bitly absorbs global traffic spikes and filters abuse at a scale few companies ever see, and that infrastructure is the second half of the bill. Your Worker gets edge scaling for free on the free tier, which covers personal use completely, but replicating their fraud filtering and multi-year data retention is where the real cost lives.
Stretch goals
- Add an expiry date and a click cap per link; past either, redirect to a fallback URL instead of showing a 404
- Put the stats view behind an /admin page guarded by a single password stored with wrangler secret put
- Import your existing Bitly slugs and destinations so printed QR codes keep working while you transition
All steps done — did it work?
Congratulations. Tell someone what you built.
About Bitly
Bitly costs $35/month. Bitly's actual product is a promise about the future: that a link printed on a box in 2019 still resolves today. No amount of code buys that, only an institution that intends to keep existing. Teams also pay for shared workspaces, SSO, and the audit trail, and marketing departments pay because the alternative is asking engineering for a redirect and waiting two weeks.
Sources & further reading
- Bitly pricing page — Shows the tier caps and costs you just sidestepped, useful context for what your free-tier build replaces.
- Sink — An open-source shortener on the same Workers, D1, and KV stack; compare its schema choices to yours after you finish.
- Shlink — A mature self-hosted shortener worth reading once you outgrow the one-sitting version.
- Dub — The largest open-source link platform; skim it for analytics ideas beyond basic click counts.
Finished alternatives (if you'd rather not build)
- Dub — Bitly with deeper attribution; self-hosting means recreating half a startup's infrastructure.
- Shlink — Branded links, routing rules and serious click stats in one container; the QR endpoint is living on borrowed time.
- Short.io — A thousand branded links, QR codes and real analytics before the pricing page becomes relevant.
- Sink — A polished Bitly clone on Cloudflare; free hosting, several bindings, and no server to patch.
Keep building
New lessons and honest build notes, by email. No spam, one-click out.
Signups open when the site goes live.