Skip to content
Y
YAS.SH
Developers

Build a URL Shortener in 100 Lines

A minimal, safe shortener you can understand end to end.

mohamed-elsaadouni10 min readapilinkstutorial
Build a URL Shortener in 100 Lines
Featured imageBuild a URL Shortener in 100 Lines

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:

  1. Scheme allow-list — only http / https. Reject javascript:, data:, etc.
  2. Host safety — reject localhost, private/loopback/metadata IPs, and embedded credentials (this is the SSRF guard).
  3. Length cap — bound the URL length (e.g. 2048 chars).
  4. No control characters — reject whitespace/control chars that can smuggle payloads.
  5. 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.

Frequently asked questions

How do short codes get generated?

Most shorteners use a random base62 string (alphabet plus numbers) of fixed length, checked for uniqueness. Some use sequential or time-based encoding for shorter, sortable codes.

Is a short link a redirect or a copy?

A short link stores a mapping and issues a 302 redirect to the destination. The shortener itself doesn't fetch the destination; it just tells the browser where to go.

What's the minimum to secure a shortener?

Validate the destination URL (http/https only, no private hosts), rate-limit creation, require authentication for custom aliases, and apply a per-IP cap. That stops most abuse.

Was this helpful? Share
🍪 Cookies & privacy. yas.sh uses only essential cookies to keep you signed in and remember your preferences. We do not run third-party trackers. See our cookie policy and privacy policy.
Settings