Build a URL shortener with real click analytics
Build snip: a small, deployable link shortener that makes short codes, redirects fast and records a privacy-minded click event every time. You get an analytics dashboard (total, by day, top referrers, by device) and the systems-design thinking behind it: how to generate short codes, why 302 and not 301, how to handle collisions, and why you cache the hot links.
- Level 0 done: Install anything from GitHub, safely and The terminal survival kit.
- uv, the Python installer from Level 0 (Install anything from GitHub has the one-line install). Check with
uv --version. It downloads Python and about 40MB of packages for you. - The same FastAPI and SQLite groundwork as Build your own API in Python. You can do this mission without it, but that one explains the pieces (Pydantic models, dependencies, status codes) at a slower pace.
- To put it online (optional): a small Linux server and a domain, as set up in Your own domain with HTTPS. Everything on your own PC is free.
What it does, and the decisions behind it
A shortener is a tiny service with some genuinely interesting choices hiding in it. Get the project running, then make the design decisions out loud before writing them down.
A URL shortener turns a long address into a short one. You share the short one; when someone opens it, the service looks up the real address and sends them there. That redirect is the one moment you can also record a click event: one row that says this link was opened, when, from where and on roughly what device. Collect enough of those and you have analytics.
You're building snip. It does four things:
POST /api/linkstakes a long URL and returns a short code, e.g.47f6QVA.GET /{code}redirects the visitor to the real URL and records one click.GET /api/links/{code}/statsreturns the analytics for that link.GET /is a dashboard page that draws those analytics.
The decisions worth making on purpose, each of which you'll meet again in the steps below:
- How to make the code. Random base62, a hash of the URL, or an auto-incrementing number encoded to base62. Step 2 picks one and says why.
- 301 or 302. A 301 (permanent) redirect is cached so hard by browsers that they stop asking you, so you stop counting. snip uses 302. Step 3.
- Collisions. Two links can land on the same random code. Rare is not never, so step 6 handles it instead of hoping.
- Open redirects. A shortener that will forward to anything is a gift to phishers. Step 3 refuses anything that isn't a plain web link.
- Privacy. A click event could hoard full IP addresses and User-Agent strings. Step 4 stores the least that still answers the question.
- Speed and scale. Almost all traffic hits a few hot links, so step 6 caches them, and step 7 talks about what changes at a million links.
Every file is in the starter kit: snip.zip (11KB). Make the project and install the two packages you need:
Terminaluv init --bare --python 3.13 snip cd snip uv add "fastapi[standard]" httpxfastapi[standard]is FastAPI plus the Uvicorn web server and thefastapicommand.httpxis the HTTP client the traffic simulator uses later. Copyshortener.py,schema.sql,simulate_clicks.pyanddashboard.htmlfrom the zip into thesnipfolder (you'll build each one up over the next steps; the kit is there to compare against or to skip the typing). Then start the server:port 8099, to stay clear of other dev serversuv run uvicorn shortener:app --port 8099OutputINFO: Started server process [31684] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8099 (Press CTRL+C to quit)In a second terminal, check it answers:
Windows: curl.execurl http://127.0.0.1:8099/healthOutput{"status":"ok"}Open 127.0.0.1:8099/docs ↗ too: FastAPI writes an interactive page from your code, and it grows as you add endpoints. Press Ctrl + C in the first terminal to stop the server.
curl http://127.0.0.1:8099/health returns {"status":"ok"}, and /docs loads.If it breaks'uv' is not recognised + 2 more
'uv' is not recognised
winget install --id=astral-sh.uv -e on Windows, brew install uv on a Mac), then open a new terminal.'error while attempting to bind on address' (port 8099 in use)
uv run uvicorn shortener:app --port 8098, and use that port everywhere below.'No module named shortener'
snip folder, or shortener.py isn't there yet. cd snip and check with dir (Windows) or ls.