A beginner-friendly, in-depth explanation of every part of this project β from scratch.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β YOUR BROWSER β
β index.html / shorten.html / analytics.html / history β
β (HTML + CSS + JavaScript) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β HTTP requests (fetch API)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Node.js Server (server/server.js) β
β β’ Receives API requests β
β β’ Runs URL shortening logic β
β β’ Serves the frontend files β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β (in-memory, same process)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β C++ Engine (url-shortener-cpp/) β
β β’ Same algorithms, implemented in C++ for learning β
β β’ Compiled separately, runs as a CLI demo β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
In simple terms:
- You open a webpage β it sends a request to the Node.js server β the server shortens the URL and sends back a short code β you share that short code β anyone who visits it gets redirected.
Every URL gets a unique number. We use a simple counter:
URL #1 β ID: 1
URL #2 β ID: 2
URL #3 β ID: 3
...
C++ file: core/Idgenerator.cpp
std::atomic<long long> counter; // atomic = thread-safe
long long getNextId() {
return counter.fetch_add(1); // returns current, then adds 1
}Why atomic? If two requests come in at the same time (multi-threading), a regular integer could give the same ID to both. atomic prevents that.
We convert the number into a short string using 62 characters: 0-9, a-z, A-Z.
ID: 1 β "1"
ID: 62 β "10" (like binary, but base 62)
ID: 12345 β "dnh"
Why Base62?
- Only URL-safe characters (no
?,&,#, etc.) - Very compact: 7 characters can represent 3.5 trillion URLs
- No ambiguous characters like
0vsO
C++ file: core/Base62Encoder.cpp
string encode(long long number) {
string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
string result = "";
while (number > 0) {
result = chars[number % 62] + result; // get remainder
number = number / 62; // divide
}
return result;
}Same logic in Node.js: server/server.js β base62Encode()
We store: "dnh" β "https://google.com"
C++ file: core/urlRepository.cpp
unordered_map<string, UrlEntry> store;
// unordered_map = hash table = O(1) lookupNode.js: server.js β const urlStore = new Map()
When someone visits localhost:3000/dnh:
- Look up
"dnh"in the store - Return
302 redirecttohttps://google.com
Browser β GET /dnh β Server β 302 β https://google.com
If 1 million people visit the same short link, we'd hit the database 1 million times. That's slow and expensive.
Keep the most recently used URLs in fast memory. Only go to the database on a cache miss.
Request for "dnh"
β
βΌ
βββββββββββββββ HIT ββββββββββββββββββββ
β LRU Cache β βββββββΊ β Return instantly β β < 1ms
β (memory) β ββββββββββββββββββββ
βββββββββββββββ
β MISS
βΌ
βββββββββββββββ ββββββββββββββββββββ
β URL Store β βββββββΊ β Return + warm β β ~1ms
β (Map/DB) β β the cache β
βββββββββββββββ ββββββββββββββββββββ
LRU = Least Recently Used β when the cache is full, evict the URL that hasn't been accessed the longest.
Cache size = 3. Access order: A, B, C, D
After A: [A]
After B: [B, A]
After C: [C, B, A]
After D: [D, C, B] β A evicted (least recently used)
Access B: [B, D, C] β B moved to front
Data structure used: doubly linked list + hash map
- Hash map: O(1) lookup by key
- Linked list: O(1) move-to-front and evict-from-back
C++ file: core/LRUCache.cpp
list<string> order; // doubly linked list (front = recent)
unordered_map<string,
pair<string, list<string>::iterator>> cache;
// map: key β (value, pointer to its position in list)Without limits, someone could send millions of requests per second and crash the server (DDoS attack).
Imagine a bucket that holds tokens:
- Bucket starts full (e.g., 10 tokens)
- Each request costs 1 token
- Tokens refill at a fixed rate (e.g., 3/second)
- If bucket is empty β request is blocked
Time 0s: [ββββββββββ] 10 tokens
Request: [βββββββββ] 9 tokens (1 used)
Request: [ββββββββ] 8 tokens
...
Time 1s: [βββββββββββ] refill +3 β capped at 10
C++ file: core/RateLimiter.cpp
void refill(Bucket& b) {
double elapsed = time_since_last_refill;
b.tokens = min(maxTokens, b.tokens + elapsed * refillRate);
}
bool allowRequest(string ip) {
refill(bucket[ip]);
if (bucket[ip].tokens >= 1) {
bucket[ip].tokens -= 1;
return true; // allowed
}
return false; // blocked
}Each IP gets its own bucket β so one abuser doesn't affect others.
TTL = Time To Live β how long a URL stays valid.
struct UrlEntry {
string longUrl;
time_point expiresAt; // when it dies
bool hasExpiry;
};When find() is called:
if (entry.hasExpiry && now > entry.expiresAt) {
store.erase(shortCode); // auto-delete
return ""; // expired
}Use cases:
- Promo links that expire after a sale
- Event links that expire after the event
- Temporary file sharing links
What if you have 3 database servers and need to decide which server stores which URL?
Naive approach: server = id % 3
- Server 0 β IDs: 0, 3, 6, 9...
- Server 1 β IDs: 1, 4, 7, 10...
- Problem: if you add a 4th server, all mappings change!
Place servers on a ring (0 to 2Β³Β²). Each URL hashes to a point on the ring, and goes to the nearest server clockwise.
Node 1
β
βββββββββββββββ
/ \
β β
Node 3 Node 2
\ /
βββββββββββββββ
When you add/remove a node, only the URLs near that node move β not everything.
C++ file: core/consistenthashing.cpp
// Virtual nodes: each real node gets 3 positions on the ring
// This ensures even distribution
ring[hash("NODE_1_0")] = 1;
ring[hash("NODE_1_1")] = 1;
ring[hash("NODE_1_2")] = 1;Counts how many times each short URL was clicked.
unordered_map<string, long long> hitCounts;
void recordHit(string shortCode) {
hitCounts[shortCode]++;
}In Node.js: entry.clicks++ every time redirect() is called.
File: server/server.js
Browser sends: { longUrl: "https://google.com", ttlSeconds: 3600 }
Server does: 1. Rate limit check
2. Validate URL format
3. Generate ID β Base62 encode
4. Save to urlStore
Server returns: { shortCode: "1", shortUrl: "http://localhost:3000/1" }
Browser visits: http://localhost:3000/1
Server does: 1. Check LRU cache
2. If miss β check urlStore
3. If expired β return 404
4. Record click (analytics)
5. Return 302 redirect
Browser goes: https://google.com β automatically
Returns summary stats: total URLs, total clicks, top URL, active count.
Returns full list of all URLs with their metadata.
Deletes a specific URL from the store and cache.
- Shows hero section with quick-shorten bar
- Calls
POST /api/shortenwhen you click "Shorten" - Calls
GET /api/analyticsto show live stats
- Full options: custom alias, TTL selector
- Calls
POST /api/shortenwith all options - Shows result with copy button and QR code modal
- QR code drawn on
<canvas>using math (no external library)
- Calls
GET /api/analyticsfor stat cards - Calls
GET /api/urlsfor the bar chart and table - Auto-refreshes every 5 seconds
- Calls
GET /api/urlsfor all links - Search, filter (active/expired), sort (newest/clicks)
- TTL progress bar shows remaining lifetime
- Calls
DELETE /api/urls/:codeto delete - QR code modal for any URL
When multiple requests come in simultaneously (concurrent users), they might try to read/write the same data at the same time. This causes race conditions β corrupted data.
Solution: std::mutex β a lock that only one thread can hold at a time.
void put(string key, string value) {
lock_guard<mutex> lock(mtx); // lock acquired
// ... modify cache ...
} // lock automatically released hereFiles with mutex: LRUCache.cpp, urlRepository.cpp, RateLimiter.cpp, AnalyticsTracker.cpp
url_shortner/
β
βββ url-shortener-cpp/ β C++ CLI demo (Phase 1-4)
β βββ core/
β β βββ Base62Encoder.* β Number β short string
β β βββ Idgenerator.* β Unique ID counter
β β βββ LRUCache.* β Fast in-memory cache
β β βββ urlrespository.h β Storage with TTL
β β βββ urlRepository.cpp β Storage implementation
β β βββ RateLimiter.* β Token bucket per IP
β β βββ consistenthashing.* β Distributed node ring
β β βββ AnalyticsTracker.* β Click counting
β β βββ QRCodeStub.h β ASCII QR placeholder
β β βββ urlshortener*.* β Main orchestrator
β βββ main.cpp β Demo runner
β
βββ server/ β Node.js HTTP backend
β βββ server.js β All API logic + server
β βββ package.json β Dependencies
β
βββ frontend/ β Web UI
β βββ index.html β Landing page
β βββ shorten.html β URL shortener form
β βββ analytics.html β Dashboard
β βββ history.html β URL history + QR
β βββ css/style.css β Shared design system
β
βββ README.md β Backend docs (per phase)
βββ FRONTEND.md β Frontend docs (per phase)
# Start the server (from project root)
cd server
node server.js
# Open in browser
http://localhost:3000That's it. The server serves both the API and the frontend files.
Base62Encoder.cppβ simplest file, understand the encoding mathIdgenerator.cppβ atomic counter, understand thread safety basicsurlRepository.cppβ simple hash map storage with TTLLRUCache.cppβ most complex data structure, doubly linked list + mapRateLimiter.cppβ token bucket algorithmurlshortservice.cppβ how all pieces connect togetherserver.jsβ same concepts in JavaScript, plus HTTP layershorten.htmlβ how the frontend calls the API