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.
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++.
307 Temporary Redirect and a Location header.
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.
#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();
}
}
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.
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.rng_ for every code, as this project does.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.
#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
What it expects
{"url": "https://example.com/very/long/path"}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.
g++ -std=c++20 -pthread -o urlshortener main.cpp UrlShortener.cpp && ./urlshortener 8080Then, 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.
$ 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 Foundparse_task_field — the key must be exactly "url" with a colon and a quoted string value.-i shows the raw response instead); add curl -L only if you actually want it to follow the redirect to the real destination.// 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.' }
}
}
- Add an expiry. Store a creation time with each entry and reject expired codes. (Teaches:
<chrono>timestamps alongside existing state.) - 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.) - 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.)
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++.