Build a URL Shortener in 100 Lines
A URL shortener looks deceptively simple — "take a long URL, return a short one, redirect when visited." But a working shortener hides real decisions: how to generate collision-free short codes, how to store the mapping, how to redirect without breaking analytics, and how to stop abuse.
This guide walks through a minimal, safe shortener end to end so you understand every moving part. The same principles scale to the full platform.
The core data model
A shortener is a mapping table. The essential fields:
id (internal)
short_code (the unique slug, e.g. "ab3x7")
original_url (the destination)
created_at (for ordering and expiry)
clicks (a counter, or a separate events table)
The only real constraint is that short_code must be unique — two codes
can't point to different things.
Generating the short code
There are two families of approaches:
1. Random. Generate a random string from a base62 alphabet
(a-z A-Z 0-9) of length N, check it's unused, retry if it collides. Simple,
unguessable-ish, and gives you a large keyspace: 7 chars ≈ 3.5 trillion
combinations.
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
bytes = random(7)
code = ""
for b in bytes: code += charset[b % 62]
2. Sequential / time-based. Encode an incrementing counter or a timestamp. Produces shorter, sortable codes (UUID v7 is one popular modern choice) but they can be guessable, so they're often combined with a random element.
For most cases, random base62 with a uniqueness check is the right default.
Storing the mapping
You need a place to persist code → URL. A key-value store or a relational table
both work. The important part is a unique index on short_code and, for
analytics, an index on the destination and created time.
A naive "check then insert" has a race (two requests grab the same code). The safe pattern is to rely on the unique index and catch the insert conflict, then regenerate or return a 409.
The redirect
When a request comes in for a code, you look it up and return a 302 redirect to the stored destination.
Why 302 and not 301? Because (as covered in the redirect status guide) a 301 is cached aggressively, so repeat visitors bypass the shortener and you lose click analytics — and you can't easily change the destination later. A 302 keeps every request hitting the shortener, so you can count clicks and re-target the link.
# pseudo
link = db.lookup(code)
if not link: return 404
return 302, Location: link.original_url
Before redirecting, also check expiry and click limits if you support them.
Counting clicks without slowing the redirect
Recording a click should never delay the redirect. The pattern is to fire the analytics write asynchronously after returning the redirect, or write to a fast queue. In practice:
- Capture the user agent, IP (hashed for privacy), referrer, and timestamp.
- Flag bots by user agent so they don't inflate real counts (see bot filtering).
- Increment a counter, but keep the redirect response immediate.
The validation that keeps it safe
A shortener that accepts arbitrary URLs is a phishing and SSRF machine unless you validate. The minimum:
- Scheme allow-list — only
http/https. Rejectjavascript:,data:, etc. - Host safety — reject localhost, private/loopback/metadata IPs, and embedded credentials (this is the SSRF guard).
- Length cap — bound the URL length (e.g. 2048 chars).
- No control characters — reject whitespace/control chars that can smuggle payloads.
- Rate limit + auth for creation — so strangers can't mint abuse codes at scale.
The redirect itself is safe because the server never fetches the destination — it
just returns a Location header.
The full flow
1. Client POST /shorten { url }
2. Validate url (scheme, host, length)
3. Authenticate + rate-limit the requester
4. Generate a unique short code
5. Insert mapping (catch unique conflicts)
6. Return 201 { short_code, url }
7. Browser GET /<code>
8. Lookup code, check expiry/click limits
9. Return 302 → destination, fire async click record
That's the entire core — and it's genuinely compact. Everything else (teams, custom domains, QR, advanced analytics) layers on top of this foundation.
The takeaway
A shortener is a small mapping table plus a redirect plus validation. The three things that separate a toy from a real one are: collision-safe code generation, async click tracking, and rigorous destination validation. Get those right and the rest is polish.
If you'd rather not build it, that's what yas.sh's API is for — the same design, with teams, analytics, QR, and abuse protection already handled.
