← thecodex.expert · The Codex Family of Knowledge
Tier 2 · Intermediate · C++ Project

URL Shortener

A URL shortener server: same hand-rolled HTTP-over-sockets shape as rest-api, with a new problem — generating short random codes that are vanishingly unlikely to collide, and proving that in a test.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a URL shortener: give it a long link, it returns a short code; give back the code, it returns the original link. It teaches two-way lookups (code↔URL), generating unique keys, and persisting a small store — the core of any link service.

Where this shows up: bit.ly and every link shortener, QR-code targets, affiliate links, any system that maps a short key to a longer value — which includes caches, session stores, and lookup services generally.

2 How to Think About It

The server plumbing is identical to rest-api’s. What is new is the Store’s randomness, and a third way (after password-generator’s template and rest-api’s plain default) to make random behaviour testable in C++.

The plan — in plain English
1. Generate a random 6-character code. → 2. Check it is not already taken, retrying on the astronomically unlikely case of a collision. → 3. Store the code-to-URL mapping, guarded by a mutex. → 4. On a GET to that code, respond with a real 307 Temporary Redirect and a Location header.

Long URL comes in

Generate a short code

Save code to URL mapping

Return the short code

Short code comes in

Look up the URL

Return the long URL

3 The Build — explained part by part

Here is the complete shortener. The Store class is where the interesting design choice lives — read its constructor first.

C++UrlShortener.hpp / UrlShortener.cpp / main.cpp
#pragma once
#include <mutex>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include <vector>

constexpr int CODE_LEN = 6;

// The shared map is code -> original URL, guarded by one std::mutex so
// every connection's thread can read and write it safely -- the same
// pattern rest-api uses, and for the same reason: without the lock, two
// shortens arriving at once could corrupt the table or hand out the same
// code twice.
//
// The random engine is seeded once in the constructor. Production code
// uses the default argument (a real std::random_device seed, confirmed
// non-deterministic on this platform in the basic-tier password-generator
// project); tests pass a fixed seed explicitly for reproducible output --
// dependency injection through a constructor parameter, a simpler
// alternative to password-generator's template-based approach for a class
// that already needs its own persistent state.
class Store {
public:
    explicit Store(unsigned seed = std::random_device{}());

    // Generates a fresh random code and checks it is not already in use,
    // retrying on the astronomically unlikely case of a collision -- a
    // real system should never assume "unlikely" means "impossible".
    // Returns std::nullopt only if the store is completely exhausted.
    std::optional<std::string> shorten(const std::string &url);

    // Looks up `code`. Returns std::nullopt if not found.
    std::optional<std::string> expand(const std::string &code) const;

private:
    mutable std::mutex mutex_;
    std::vector<std::pair<std::string, std::string>> entries_; // code -> url
    std::mt19937 rng_;

    std::string generate_code(); // caller must hold mutex_
};

struct Response {
    std::string status;
    std::string location;
    std::string body;
};

// The whole route table, kept separate from socket handling so it can be
// tested by calling it directly with a fake request -- no network
// involved.
Response handle_request(Store &store, const std::string &method,
                         const std::string &path, const std::string &body);

#include "UrlShortener.hpp"
#include <algorithm>

Store::Store(unsigned seed) : rng_(seed) {}

std::string Store::generate_code() {
    static const std::string alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    std::uniform_int_distribution<std::size_t> dist(0, alphabet.size() - 1);
    std::string code(CODE_LEN, ' ');
    for (auto &c : code) c = alphabet[dist(rng_)];
    return code;
}

std::optional<std::string> Store::shorten(const std::string &url) {
    std::lock_guard<std::mutex> lock(mutex_);
    // 62^6 possible codes -- a retry loop with a generous cap is a formality,
    // not a real limit, but a real system should never assume "unlikely"
    // means "impossible".
    for (int attempt = 0; attempt < 1000; attempt++) {
        std::string code = generate_code();
        bool exists = std::any_of(entries_.begin(), entries_.end(),
                                   [&code](const auto &e) { return e.first == code; });
        if (!exists) {
            entries_.emplace_back(code, url);
            return code;
        }
    }
    return std::nullopt;
}

std::optional<std::string> Store::expand(const std::string &code) const {
    std::lock_guard<std::mutex> lock(mutex_);
    auto it = std::find_if(entries_.begin(), entries_.end(),
                            [&code](const auto &e) { return e.first == code; });
    if (it == entries_.end()) return std::nullopt;
    return it->second;
}

static std::optional<std::string> parse_url_field(const std::string &body) {
    auto key = body.find("\"url\"");
    if (key == std::string::npos) return std::nullopt;
    auto colon = body.find(':', key);
    if (colon == std::string::npos) return std::nullopt;
    auto open_quote = body.find('"', colon);
    if (open_quote == std::string::npos) return std::nullopt;
    auto close_quote = body.find('"', open_quote + 1);
    if (close_quote == std::string::npos) return std::nullopt;
    return body.substr(open_quote + 1, close_quote - open_quote - 1);
}

Response handle_request(Store &store, const std::string &method,
                         const std::string &path, const std::string &body) {
    if (method == "POST" && path == "/shorten") {
        auto url = parse_url_field(body);
        if (!url) return Response{"400 Bad Request", "", "{\"error\":\"missing url field\"}"};
        auto code = store.shorten(*url);
        if (!code) return Response{"507 Insufficient Storage", "", "{\"error\":\"store is full\"}"};
        return Response{"201 Created", "", "{\"code\":\"" + *code + "\"}"};
    }
    if (method == "GET" && path.size() > 1 && path[0] == '/') {
        std::string code = path.substr(1);
        auto url = store.expand(code);
        if (!url) return Response{"404 Not Found", "", "{\"error\":\"no such code\"}"};
        return Response{"307 Temporary Redirect", *url, ""};
    }
    return Response{"404 Not Found", "", "{\"error\":\"no such route\"}"};
}

#define _POSIX_C_SOURCE 200809L
#include "UrlShortener.hpp"
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sstream>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>

static std::string read_request(int fd) {
    std::string data;
    char buf[4096];
    size_t header_end = std::string::npos;
    ssize_t n;
    while ((header_end = data.find("\r\n\r\n")) == std::string::npos) {
        n = recv(fd, buf, sizeof(buf), 0);
        if (n <= 0) return data;
        data.append(buf, static_cast<size_t>(n));
    }
    size_t content_length = 0;
    auto cl_pos = data.find("Content-Length:");
    if (cl_pos != std::string::npos && cl_pos < header_end) {
        content_length = static_cast<size_t>(std::stoi(data.substr(cl_pos + 15)));
    }
    size_t body_have = data.size() - (header_end + 4);
    while (body_have < content_length) {
        n = recv(fd, buf, sizeof(buf), 0);
        if (n <= 0) break;
        data.append(buf, static_cast<size_t>(n));
        body_have = data.size() - (header_end + 4);
    }
    return data;
}

static void serve_connection(int client_fd, Store *store) {
    std::string request = read_request(client_fd);
    std::istringstream lines(request);
    std::string request_line;
    std::getline(lines, request_line);
    std::istringstream rl(request_line);
    std::string method, path, version;
    rl >> method >> path >> version;

    auto header_end = request.find("\r\n\r\n");
    std::string body = header_end == std::string::npos ? "" : request.substr(header_end + 4);

    Response resp = handle_request(*store, method, path, body);

    std::ostringstream out;
    out << "HTTP/1.1 " << resp.status << "\r\n";
    if (!resp.location.empty()) out << "Location: " << resp.location << "\r\n";
    out << "Content-Type: application/json\r\n"
        << "Content-Length: " << resp.body.size() << "\r\n"
        << "Connection: close\r\n\r\n"
        << resp.body;
    std::string response = out.str();
    send(client_fd, response.c_str(), response.size(), 0);
    close(client_fd);
}

int main(int argc, char **argv) {
    int port = argc > 1 ? std::stoi(argv[1]) : 8080;
    Store store;

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(static_cast<uint16_t>(port));
    if (bind(server_fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
        std::cerr << "bind failed\n";
        return 1;
    }
    listen(server_fd, 16);
    std::cout << "Listening on port " << port << "\n";

    while (true) {
        int client_fd = accept(server_fd, nullptr, nullptr);
        if (client_fd < 0) continue;
        std::thread(serve_connection, client_fd, &store).detach();
    }
}
⚠ No in-browser playground here
C++ compiles to a real, native binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once a C++17-or-newer compiler like g++ or clang++ is installed.
What each part does — in plain words
explicit Store(unsigned seed = std::random_device{}()); — the constructor takes a seed with a default value drawn from real entropy. Production code calls Store store; and gets genuine, unpredictable codes; the tests call Store store(42); and get fully reproducible ones. This is a simpler way to make randomness testable than password-generator’s template-based RNG injection — appropriate here because Store already has its own persistent state to own the generator inside, whereas generate_password was a free function with nothing to hold a member variable in.

std::uniform_int_distribution<std::size_t> dist(0, alphabet.size() - 1); — the same bias-free way of picking a random index that password-generator used, reused here for the same reason: rng() % alphabet.size() would subtly favour the lowest indices.

for (int attempt = 0; attempt < 1000; attempt++) { ... } in shorten — a collision-retry loop, exactly mirroring the C version’s arc4random_uniform-based approach: with 62 possible characters across 6 positions, a collision is astronomically unlikely, but “unlikely” is never the same guarantee as “impossible,” so the code checks anyway rather than assuming.

Response{"307 Temporary Redirect", *url, ""} — a real HTTP redirect, with the destination carried in the Location header (wired up in main.cpp), not just a plain-text response telling the caller where to go.
Common mistakes — and how to avoid them
✗ Seeding a fresh std::mt19937 with std::random_device{}() inside generate_code, called on every single shorten — reseeding constantly is wasteful and can reduce randomness quality, the same mistake flagged for number-guessing-game in the basic tier.
✓ Seed the engine once, in the constructor, and reuse it via the member rng_ for every code, as this project does.
✗ Returning 302 Found for the redirect — a common default in tutorials, but semantically wrong here, since 302 traditionally implies the redirect might change (some clients even re-POST to it), whereas a short URL always points to the same destination.
✓ Use 307 Temporary Redirect (or 301/308 if the mapping is meant to be permanent), as this project does, so the HTTP status code says what actually happens.

4 Test & Prove Each Part

Seven checks: code length and uniqueness, the expand round-trip, an unknown-code lookup, and — the one worth reading closely — a test that two Store instances built with the same seed produce the exact same first code, proving the constructor-seed approach is genuinely deterministic.

shorten returns a code of the expected length
expand finds the original URL behind a shortened code
expand returns std::nullopt for an unknown code
Two different URLs get two different codes
The same seed produces the same code, deterministically
handle_request: shorten then expand round-trips through the real route table
handle_request GET on an unknown code returns 404
C++test_UrlShortener.cpp
#include "UrlShortener.hpp"
#include <cassert>
#include <iostream>

#define RUN(name) do { name(); std::cout << "PASS: " << #name << "\n"; } while (0)

static void shorten_returns_a_code_of_the_expected_length() {
    Store store(42); // fixed seed -- deterministic and reproducible for a test
    auto code = store.shorten("https://example.com/a/very/long/path");
    assert(code.has_value());
    assert(code->size() == static_cast<size_t>(CODE_LEN));
}

static void expand_finds_the_url_behind_a_shortened_code() {
    Store store(7);
    auto code = store.shorten("https://example.com/page");
    assert(code.has_value());
    auto url = store.expand(*code);
    assert(url.has_value());
    assert(*url == "https://example.com/page");
}

static void expand_returns_nullopt_for_an_unknown_code() {
    Store store(1);
    assert(!store.expand("zzzzzz").has_value());
}

static void two_different_urls_get_two_different_codes() {
    Store store(99);
    auto code1 = store.shorten("https://example.com/one");
    auto code2 = store.shorten("https://example.com/two");
    assert(code1.has_value() && code2.has_value());
    assert(*code1 != *code2);
}

static void the_same_seed_produces_the_same_code_deterministically() {
    Store a(123);
    Store b(123);
    auto code_a = a.shorten("https://example.com/x");
    auto code_b = b.shorten("https://example.com/x");
    assert(code_a.has_value() && code_b.has_value());
    assert(*code_a == *code_b); // same seed, same first draw -- fully reproducible
}

static void handle_request_shorten_then_expand_round_trips() {
    Store store(55);
    Response shorten_resp = handle_request(store, "POST", "/shorten",
                                            R"({"url": "https://example.com/z"})");
    assert(shorten_resp.status == "201 Created");
    auto code_pos = shorten_resp.body.find("\"code\":\"");
    assert(code_pos != std::string::npos);
    std::string code = shorten_resp.body.substr(code_pos + 8, CODE_LEN);

    Response expand_resp = handle_request(store, "GET", "/" + code, "");
    assert(expand_resp.status == "307 Temporary Redirect");
    assert(expand_resp.location == "https://example.com/z");
}

static void handle_request_get_unknown_code_is_a_404() {
    Store store(1);
    Response r = handle_request(store, "GET", "/nosuchcode", "");
    assert(r.status == "404 Not Found");
}

int main() {
    RUN(shorten_returns_a_code_of_the_expected_length);
    RUN(expand_finds_the_url_behind_a_shortened_code);
    RUN(expand_returns_nullopt_for_an_unknown_code);
    RUN(two_different_urls_get_two_different_codes);
    RUN(the_same_seed_produces_the_same_code_deterministically);
    RUN(handle_request_shorten_then_expand_round_trips);
    RUN(handle_request_get_unknown_code_is_a_404);
    std::cout << "All tests passed.\n";
    return 0;
}

Compile and run with g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread -o test_run UrlShortener.cpp test_UrlShortener.cpp && ./test_run.

5 The Interface

INPUTPOST /shortena JSON body like {"url": "..."}
What it expects
{"url": "https://example.com/very/long/path"}
OUTPUTOUTPUTa short code, or a 307 redirect when visited
What it returns
{"code":"2Yy1tA"}

6 Run It & Automate It

Save the code as UrlShortener.hpp / UrlShortener.cpp / main.cpp and compile it with g++ — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.

Run it locally
g++ -std=c++20 -pthread -o urlshortener main.cpp UrlShortener.cpp && ./urlshortener 8080
Then, from another terminal, talk to it with curl.

A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
$ curl -X POST http://127.0.0.1:8080/shorten -d '{"url": "https://example.com/very/long/path"}'
{"code":"2Yy1tA"}
$ curl -i http://127.0.0.1:8080/2Yy1tA
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/very/long/path
...
$ curl -i http://127.0.0.1:8080/doesnotexist
HTTP/1.1 404 Not Found
If it breaks — how to fix it
🚨 {"error":"missing url field"}
Same hand-written body search as rest-api’s parse_task_field — the key must be exactly "url" with a colon and a quoted string value.
🚨 curl follows the redirect and shows an error page instead of the Location header.
That is curl doing its job (-i shows the raw response instead); add curl -L only if you actually want it to follow the redirect to the real destination.
GroovyJenkinsfile
// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
    agent any

    stages {
        stage('Get the code') {
            // download the latest code
            steps { checkout scm }
        }
        stage('Compile') {
            steps {
                // confirm a compiler is installed
                sh 'g++ --version'
                // compile with strict warnings on
                sh 'g++ -std=c++20 -Wall -Wextra -o app *.cpp -pthread'
            }
        }
        stage('Run the tests') {
            steps {
                // prints PASS/FAIL, exits non-zero on failure
                sh './app'
            }
        }
        stage('Check for memory leaks') {
            steps {
                // fails the build on any leak or invalid access
                sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
            }
        }
    }

    post {
        success { echo 'All tests passed, no leaks found.' }
        failure { echo 'A test or Valgrind check failed — see above.' }
    }
}
🎯 Try this next — make it yours
  1. Add an expiry. Store a creation time with each entry and reject expired codes. (Teaches: <chrono> timestamps alongside existing state.)
  2. Let the caller request a custom code. {"url": "...", "custom": "my-code"}, falling back to random generation if omitted. (Teaches: an optional field in the same hand-written body parser.)
  3. Persist the store to disk, the way cli-task-manager and to-do-list already do, so shortened URLs survive a restart. (Teaches: applying an already-learned serialization pattern to a new, mutex-guarded class.)
What you learned
You learned a third way to make randomness testable in C++ — seeding through a constructor parameter with a real-entropy default — alongside password-generator’s template approach, reused std::uniform_int_distribution to avoid modulo bias, and saw why a URL shortener’s redirect should be a real 307 status code with a Location header rather than a plain-text answer. Related: Modern C++ (C++11–C++23), Concurrency in C++.