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.
The core: a redirect and a store
The heart of a URL shortener is deceptively simple: a store mapping short codes to destinations, and a route that looks up the code and issues a redirect. When a request arrives for /abc123, the server finds the matching destination and responds with a redirect to it. Everything else — analytics, auth, custom aliases, expiry, rate limits — is layered on top of this small core. Understanding that the core is just "a lookup and a redirect" is what keeps the design honest and prevents over-engineering.
The decisions that actually matter
Building a working shortener is easy; building one that is safe and dependable takes a few deliberate decisions:
- The short code. A random base62 string (letters and digits) of a chosen length gives a large, collision-resistant namespace. Decide the length by how many links you expect; seven characters covers trillions of combinations.
- The redirect type. A 302 is the common default because it keeps the visitor passing through the short-link layer for tracking; a 301 suits permanent moves. The redirect 301 vs 302 guide details the trade-off.
- Validation. Reject or normalize destinations that are unsafe — no
javascript:, no private/loopback addresses, no obvious abuse. Link shorteners are an abuse vector, so validation is a security control, not a nicety. - Rate limiting. Bound the endpoint so a burst cannot overwhelm it. This is the same discipline as the rate limits guide.
Getting these four right is most of the engineering value.
The layer that adds the real value
A bare redirector is commodity; the value comes from the layers around it. Click analytics turn the redirect into a measurement point. Custom aliases and custom domains turn it into a branded tool. Expiry, password gates, and teams turn it into a governed platform. When you design the shortener, keep the core clean and build these as separable layers, so each can grow and be secured independently. The link analytics and teams and permissions guides describe the layers this core supports.
Knowing what not to build
It is just as important to know what to reuse. A robust, maintained shortener needs SSRF protection, bot filtering, abuse response, uptime, and security hardening — a lot to build well from scratch. If your goal is infrastructure rather than learning, using a proven platform for the hard parts while keeping your own logic on top is usually the better trade than rebuilding the security layer yourself. The honest engineering question is where your unique value is, and to build only there.
