Get Started

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.

115 minAdvancedTools: FreeStep 1 free
Tested 12 Oct 2026 · FastAPI 0.143.0, Starlette 1.7.0, Pydantic 2.14.1, uvicorn 0.54.0 and httpx 0.28.1 on Python 3.13.5 via uv 0.11.9, screenshot with Playwright 1.64, Windows 11
A working FastAPI service in one fileshorten a URL, redirect with a 302, and record a click event, stored in one SQLite database with a schema you can read
An analytics dashboard page plus a JSON APItotal clicks, clicks per day, top referrers and a device breakdown, all from GROUP BY queries
The systems-design decisions made explicit, with a hot-link cache, collision handling, an open-redirect guard and a click record that stores the least it can get away with
Before you start
01/07What it does, and the decisions behind it0 done
STEP 01 of 07~12 min

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.

  1. 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.

  2. You're building snip. It does four things:

    • POST /api/links takes 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}/stats returns the analytics for that link.
    • GET / is a dashboard page that draws those analytics.
  3. 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.
  4. Every file is in the starter kit: snip.zip (11KB). Make the project and install the two packages you need:

    Terminal
    uv init --bare --python 3.13 snip
    cd snip
    uv add "fastapi[standard]" httpx
  5. fastapi[standard] is FastAPI plus the Uvicorn web server and the fastapi command. httpx is the HTTP client the traffic simulator uses later. Copy shortener.py, schema.sql, simulate_clicks.py and dashboard.html from the zip into the snip folder (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 servers
    uv run uvicorn shortener:app --port 8099
    Output
    INFO:     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)
  6. In a second terminal, check it answers:

    Windows: curl.exe
    curl http://127.0.0.1:8099/health
    Output
    {"status":"ok"}
  7. 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.

✓
Checkpointcurl 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
Install it as in Install anything from GitHub (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)
Something else is on 8099. Pick another port: uv run uvicorn shortener:app --port 8098, and use that port everywhere below.
'No module named shortener'
You're not in the snip folder, or shortener.py isn't there yet. cd snip and check with dir (Windows) or ls.
Still stuck? Ask the community
Next mission · Advanced buildsBuild a real CI/CD pipeline with GitHub Actions
→