
URL Shortener as a Backend Learning Project. A Build Next Stack field guide for learners shipping small projects.
Scope: shorten, redirect, list — nothing else
A URL shortener is the smallest useful backend: accept a long URL, store a mapping to a short slug, redirect GET requests with 302 or 301, and list mappings for debugging. No accounts, no click analytics dashboard, no custom domains. Storage: SQLite table links(id, slug UNIQUE, url, created_at).
You learn slug generation, uniqueness constraints, redirect status codes, and validation of untrusted URLs. Framework optional—bare HTTP server plus stdlib is valid if routing stays readable. Done means curl creates a short link, browser follows redirect to the original, duplicate long URL returns same slug or a clear policy you document.
Draw a sequence diagram on paper: client POST, server insert, client GET redirect, server lookup. If you cannot sketch it in ninety seconds, routing or naming is too clever. Shorteners are teaching tools because the flow fits one sticky note.
Milestone one: slug generation and storage
POST /shorten body {“url”: “https://example.com/path”}. Validate:
- Scheme is http or https.
- Length under sensible max (2048).
- Reject javascript: and data: schemes outright.
Generate slug: random six-character base62 unless you prefer hash truncated—document collision retry (max three attempts, then 500). Insert row; return 201 with {“slug”: “a1B9x2”, “short_url”: “http://localhost:8000/a1B9x2”}. On duplicate slug collision, retry silently; on duplicate URL, pick one policy: return existing slug (200) or create new slug—state it in README.
Index the slug column uniquely in SQLite from day one. Full-table scan redirects work for ten rows and fail silently at ten thousand. Schema migrations on SQLite are painful; get indexes right before deploy.
Acceptance: two different URLs get different slugs; same URL twice behaves per your documented policy.
Milestone two: redirect with correct status codes
GET /:slug looks up slug; if missing, 404 plain text not found. If found, redirect with 302 Found for learning projects (safer while URLs change). Explain in comments when you would choose 301 for permanent links.
GET /admin/links returns JSON array for local debugging—protect with env flag ADMIN=1 only, never expose on public deploy without auth. This endpoint saves hours when you forget what slug maps where.
Test redirect with curl -I to inspect Location header without following. Browser test is second, not first—scripts catch regressions faster.
Measure redirect latency locally with curl -w ‘%{time_total}n’. Sub-ten-millisecond lookups on SQLite prove indexing works; full-table scans show up as hundreds of milliseconds before data grows large.
Acceptance checks for duplicate URLs and bad input
Checklist:
- Invalid URL → 400 JSON error, no DB row.
- Valid URL → slug length fixed, charset alphanumeric.
- Unknown slug → 404, not 500.
- Redirect Location matches stored URL exactly, including query strings.
- 10k sequential creates without unique constraint violations—run a small script overnight or capped loop.
Store created_at in UTC ISO format. Listing sorted newest first makes manual inspection pleasant during development.
Document slug length and charset in README so future you does not shorten slugs and break old links. Link rot in your own shortener teaches why migration plans matter before production.
Traps that break the redirect lesson
Trap: slugs that look like routes. Reserve paths /admin, /health, /shorten before catch-all slug route—or use prefix /s/:slug.
Trap: open redirect abuse. Do not accept arbitrary url query params on GET without validation. Your POST body validator is the gate.
Trap: logging full URLs with tokens. Strip query strings in logs or redact known sensitive params when debugging.
Trap: analytics before correctness. Click counters invite race conditions. Count later; redirect must be correct now.
Trap: deployment without HTTPS. Localhost HTTP is fine; public deploy should sit behind TLS terminator so browsers trust redirects.
Trap: shortening URLs you do not control. Shortening a login page with session tokens in query strings leaks credentials in referrer headers when users navigate away. Document that users should not shorten authenticated URLs—validation cannot fix all social issues.
Minimal tests worth writing before deploy
Four tests cover v1: create valid link, reject bad scheme, redirect known slug, 404 unknown slug. Use in-memory SQLite or temp file; do not hit production DB. HTTP tests through the framework client beat mocking every layer.
Snapshot the JSON shape of shorten response—field names stable even when slug algorithm changes. Clients break on renamed keys, not on shorter slugs.
Run tests in CI on push even if deploy is manual. Redirect regressions are silent until a user clicks an old link; tests are cheaper than apology emails.
Security basics without building auth yet
Rate-limit shorten endpoint naively: in-memory counter per IP per minute acceptable for learning. Add CAPTCHA only if you expose publicly and see abuse—out of scope for week one.
Sanitize stored URLs but redirect to exact stored value—do not “helpfully” rewrite user URLs unless you document it. SSRF is not a major risk when users submit URLs browsers will follow anyway; still block file:// and internal IP ranges if your validator fetches URLs server-side later.
When you deploy, set ADMIN=0 and remove list route from production build or guard with a random query token in env. Document threat model in five sentences: who can create links, who can enumerate, what data leaks.
Health endpoint GET /health returns plain OK for uptime monitors—two lines, no framework magic. Knowing how to add health checks early saves deploy confusion later on every backend project.
Shorten three links you use this week
Create slugs for a long docs URL, a calendar invite, and a repo README anchor. Put them in a sticky note; when redirect fails, fix before adding analytics. Real links surface encoding bugs fake example.com never will.
Write four curl examples in README: shorten, redirect head request, list admin, invalid URL. That file becomes your manual test suite until you add formal tests.
The shortener fits in one evening once you respect scope. Resist custom slugs and QR codes until redirects are boringly reliable—that boredom is backend competence.