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

URL Shortener

A complete small HTTP service: POST a long URL, get a short code back, and GET the code to be redirected to the original. Hand-rolled sockets, a mutex-guarded table, and a real collision-avoidance loop.

🧠 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

Two operations, one shared table: shorten a URL into a code, and expand a code back into its URL.

The plan — in plain English
1. On POST /shorten, generate a random code, checking it is not already taken. → 2. Store code → URL in a shared, mutex-guarded table. → 3. On GET /{code}, look the code up and reply with a real HTTP redirect. → 4. If the code is unknown, reply 404.

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 service, built on the same hand-rolled HTTP server shape as the REST API project — a raw listening socket, one pthread per connection, and a mutex-guarded store, since C has no built-in web framework to reach for.

CUrlShortener.h / UrlShortener.c / main.c
#ifndef URL_SHORTENER_H
#define URL_SHORTENER_H
#include <pthread.h>

#define MAX_URLS 4096
#define CODE_LEN 6
#define MAX_URL 512

/* The shared map is code -> original URL, guarded by one mutex so every
 * connection's thread can read and write it safely -- the same pattern the
 * rest-api project 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. */
typedef struct {
    char code[CODE_LEN + 1];
    char url[MAX_URL];
} Entry;

typedef struct {
    Entry entries[MAX_URLS];
    int count;
    pthread_mutex_t lock;
} Store;

void store_init(Store *store);

/* 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". Copies the
 * chosen code into `out_code` (must hold CODE_LEN + 1 bytes) and returns 1,
 * or 0 if the store is completely full. */
int store_shorten(Store *store, const char *url, char *out_code);

/* Looks up `code`, copying its URL into `out` (must hold MAX_URL bytes).
 * Returns 1 if found, 0 otherwise. */
int store_expand(const Store *store, const char *code, char *out);

typedef struct {
    char status[32];
    char location[MAX_URL];
    char body[MAX_URL];
} Response;

/* 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 char *method, const char *path, const char *body);

#endif

#define _DEFAULT_SOURCE
#include "UrlShortener.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>

/* arc4random_uniform is a real, cryptographically secure function available
 * directly in this system's glibc -- no external library needed, the same
 * function password-generator uses. A short code only needs to be
 * hard-to-guess-in-advance, not secret, but there is no reason to reach for
 * a weaker generator when a secure one is free. */
static const char *CODE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

void store_init(Store *store) {
    store->count = 0;
    pthread_mutex_init(&store->lock, NULL);
}

static int find_index(const Store *store, const char *code) {
    for (int i = 0; i < store->count; i++) {
        if (strcmp(store->entries[i].code, code) == 0) return i;
    }
    return -1;
}

static void make_code(char *out) {
    size_t chars_len = strlen(CODE_CHARS);
    for (int i = 0; i < CODE_LEN; i++) {
        out[i] = CODE_CHARS[arc4random_uniform((unsigned int)chars_len)];
    }
    out[CODE_LEN] = '\0';
}

int store_shorten(Store *store, const char *url, char *out_code) {
    pthread_mutex_lock(&store->lock);
    if (store->count >= MAX_URLS) { pthread_mutex_unlock(&store->lock); return 0; }

    char code[CODE_LEN + 1];
    do {
        make_code(code);
    } while (find_index(store, code) != -1); /* keep retrying on the (astronomically unlikely) collision */

    strcpy(store->entries[store->count].code, code);
    char *dest = store->entries[store->count].url;
    strncpy(dest, url, MAX_URL - 1);
    dest[MAX_URL - 1] = '\0';
    store->count++;
    strcpy(out_code, code);

    pthread_mutex_unlock(&store->lock);
    return 1;
}

int store_expand(const Store *store, const char *code, char *out) {
    int idx = find_index(store, code);
    if (idx < 0) return 0;
    strncpy(out, store->entries[idx].url, MAX_URL - 1);
    out[MAX_URL - 1] = '\0';
    return 1;
}

static void trim(const char *s, char *out, int out_cap) {
    while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
    size_t len = strlen(s);
    while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
    if (len >= (size_t)out_cap) len = (size_t)out_cap - 1;
    memcpy(out, s, len);
    out[len] = '\0';
}

Response handle_request(Store *store, const char *method, const char *path, const char *body) {
    Response r;
    r.location[0] = '\0';

    if (strcmp(method, "POST") == 0 && strcmp(path, "/shorten") == 0) {
        char url[MAX_URL];
        trim(body, url, sizeof(url));
        if (url[0] == '\0') {
            strcpy(r.status, "400 Bad Request");
            strcpy(r.body, "empty body");
            return r;
        }
        char code[CODE_LEN + 1];
        store_shorten(store, url, code);
        strcpy(r.status, "201 Created");
        snprintf(r.body, sizeof(r.body), "http://127.0.0.1:8081/%s", code);
        return r;
    }

    if (strcmp(method, "GET") == 0 && strlen(path) > 1) {
        const char *code = path + 1;
        char url[MAX_URL];
        if (store_expand(store, code, url)) {
            strcpy(r.status, "307 Temporary Redirect");
            strncpy(r.location, url, sizeof(r.location) - 1);
            r.location[sizeof(r.location) - 1] = '\0';
            r.body[0] = '\0';
        } else {
            strcpy(r.status, "404 Not Found");
            strcpy(r.body, "no such short link");
        }
        return r;
    }

    strcpy(r.status, "404 Not Found");
    strcpy(r.body, "no such route");
    return r;
}

#define _POSIX_C_SOURCE 200809L
#include "UrlShortener.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8081
#define MAX_REQUEST 8192

static int read_request(int fd, char *method, int method_cap, char *path, int path_cap,
                         char *body, int body_cap) {
    static char buf[MAX_REQUEST];
    int total = 0, header_end = -1;

    while (total < MAX_REQUEST - 1) {
        ssize_t n = recv(fd, buf + total, (size_t)(MAX_REQUEST - 1 - total), 0);
        if (n <= 0) return 0;
        total += (int)n;
        buf[total] = '\0';
        char *sep = strstr(buf, "\r\n\r\n");
        if (sep) { header_end = (int)(sep - buf) + 4; break; }
    }
    if (header_end < 0) return 0;

    int content_length = 0;
    char *cl = strstr(buf, "Content-Length:");
    if (cl && cl < buf + header_end) content_length = atoi(cl + strlen("Content-Length:"));

    int body_have = total - header_end;
    while (body_have < content_length && total < MAX_REQUEST - 1) {
        ssize_t n = recv(fd, buf + total, (size_t)(MAX_REQUEST - 1 - total), 0);
        if (n <= 0) break;
        total += (int)n;
        buf[total] = '\0';
        body_have = total - header_end;
    }

    char req_line[512];
    char *line_end = strstr(buf, "\r\n");
    size_t line_len = line_end ? (size_t)(line_end - buf) : strlen(buf);
    if (line_len >= sizeof(req_line)) line_len = sizeof(req_line) - 1;
    memcpy(req_line, buf, line_len);
    req_line[line_len] = '\0';

    char *sp1 = strchr(req_line, ' ');
    if (!sp1) return 0;
    *sp1 = '\0';
    strncpy(method, req_line, (size_t)method_cap - 1);
    method[method_cap - 1] = '\0';

    char *sp2 = strchr(sp1 + 1, ' ');
    size_t path_len = sp2 ? (size_t)(sp2 - (sp1 + 1)) : strlen(sp1 + 1);
    if (path_len >= (size_t)path_cap) path_len = (size_t)path_cap - 1;
    memcpy(path, sp1 + 1, path_len);
    path[path_len] = '\0';

    int copy_len = body_have < body_cap - 1 ? body_have : body_cap - 1;
    if (copy_len > 0) memcpy(body, buf + header_end, (size_t)copy_len);
    body[copy_len > 0 ? copy_len : 0] = '\0';
    return 1;
}

typedef struct {
    Store *store;
    int fd;
} ConnArgs;

static void *serve_connection(void *arg) {
    ConnArgs *args = arg;
    char method[16], path[256], body[MAX_URL];
    if (read_request(args->fd, method, sizeof(method), path, sizeof(path), body, sizeof(body))) {
        Response resp = handle_request(args->store, method, path, body);
        char out[MAX_URL * 2];
        int len = snprintf(out, sizeof(out), "HTTP/1.1 %s\r\nContent-Length: %zu\r\n",
                            resp.status, strlen(resp.body));
        if (resp.location[0] != '\0') {
            len += snprintf(out + len, sizeof(out) - (size_t)len, "Location: %s\r\n", resp.location);
        }
        len += snprintf(out + len, sizeof(out) - (size_t)len, "Connection: close\r\n\r\n%s", resp.body);
        send(args->fd, out, (size_t)len, 0);
    }
    close(args->fd);
    free(args);
    return NULL;
}

int main(void) {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket"); return 1; }
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("could not bind to :8081");
        return 1;
    }
    if (listen(listen_fd, 16) < 0) { perror("listen"); return 1; }

    static Store store;
    store_init(&store);
    printf("Listening on http://127.0.0.1:%d\n", PORT);

    for (;;) {
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0) { perror("accept"); continue; }
        ConnArgs *args = malloc(sizeof(ConnArgs));
        args->store = &store;
        args->fd = client_fd;
        pthread_t thread;
        pthread_create(&thread, NULL, serve_connection, args);
        pthread_detach(thread);
    }
}
⚠ 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 GCC or Clang is installed.
What each part does — in plain words
do { make_code(code); } while (find_index(store, code) != -1); — keeps generating a fresh random code until it finds one not already in the table. At 6 characters from a 62-character alphabet there are over 56 billion possible codes, so a collision is astronomically unlikely at this scale — but “unlikely” is not “impossible,” and the loop costs almost nothing, so there is no reason to skip checking.

arc4random_uniform — the same real, cryptographically secure function password-generator uses, confirmed available directly in this glibc with no external library. A short code only needs to be hard to guess in advance, not secret, but there is no reason to reach for a weaker generator when a secure one is free and already in the standard library.

307 Temporary Redirect + a Location header — the actual HTTP mechanism a browser uses to follow a short link to its real destination. 307 specifically preserves the original request method on the redirect, unlike a 301/302, which is the more correct choice for a general-purpose redirect service.

typedef struct { char status[32]; char location[MAX_URL]; char body[MAX_URL]; } Response; — unlike the REST API project’s response, this one carries an explicit location field, because a redirect is defined entirely by its Location header, not its body; main.c only writes that header line when location is non-empty.
Common mistakes — and how to avoid them
✗ Generating a code and storing it without checking whether it is already taken — rare, but a real collision would silently overwrite someone else’s short link.
✓ Loop until you generate a code that is not already a key in the table, as store_shorten does.
✗ Returning a 301/302 redirect for every case — browsers and some HTTP clients are allowed to change a POST into a GET when following those codes.
✓ Use 307 Temporary Redirect (or 308 for a permanent one) when the request method must be preserved.

4 Test & Prove Each Part

We test the shortening/expansion logic directly, including that a pre-existing entry survives a new shorten call, plus one end-to-end pass through the routing function.

A shortened URL expands back to the original
Expanding an unknown code returns 0, not a crash
A pre-existing entry is never disturbed by a later shorten call
The full POST /shorten -> GET /{code} flow returns a real redirect with the right Location header
An empty body on POST /shorten is a 400
An unknown route is a 404
Ctest_UrlShortener.c
#include "UrlShortener.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>

#define RUN(name) do { name(); printf("PASS: %s\n", #name); } while (0)

static void shortens_and_expands_a_url(void) {
    Store store;
    store_init(&store);
    char code[CODE_LEN + 1];
    store_shorten(&store, "https://example.com/a/very/long/path", code);
    assert(strlen(code) == CODE_LEN);

    char url[MAX_URL];
    assert(store_expand(&store, code, url));
    assert(strcmp(url, "https://example.com/a/very/long/path") == 0);
}

static void expanding_an_unknown_code_returns_zero(void) {
    Store store;
    store_init(&store);
    char url[MAX_URL];
    assert(!store_expand(&store, "nosuch", url));
}

static void shorten_never_disturbs_an_existing_entry(void) {
    Store store;
    store_init(&store);
    /* Pre-fill an entry directly, the same way the original Rust test forces
     * a collision scenario: prove a fresh shorten() never touches it. */
    strcpy(store.entries[0].code, "AAAAAA");
    strcpy(store.entries[0].url, "https://taken.example");
    store.count = 1;

    char code[CODE_LEN + 1];
    store_shorten(&store, "https://new.example", code);

    char url[MAX_URL];
    assert(store_expand(&store, code, url));
    assert(strcmp(url, "https://new.example") == 0);
    assert(store_expand(&store, "AAAAAA", url));
    assert(strcmp(url, "https://taken.example") == 0);
}

static void http_route_end_to_end_through_handle_request(void) {
    Store store;
    store_init(&store);
    Response shorten = handle_request(&store, "POST", "/shorten", "https://example.com");
    assert(strcmp(shorten.status, "201 Created") == 0);
    const char *slash = strrchr(shorten.body, '/');
    assert(slash != NULL);
    const char *code = slash + 1;

    char path[64];
    snprintf(path, sizeof(path), "/%s", code);
    Response redirect = handle_request(&store, "GET", path, "");
    assert(strcmp(redirect.status, "307 Temporary Redirect") == 0);
    assert(strcmp(redirect.location, "https://example.com") == 0);
}

static void an_empty_body_shorten_is_a_400(void) {
    Store store;
    store_init(&store);
    Response r = handle_request(&store, "POST", "/shorten", "   ");
    assert(strcmp(r.status, "400 Bad Request") == 0);
}

static void unknown_route_is_404(void) {
    Store store;
    store_init(&store);
    Response r = handle_request(&store, "GET", "/", "");
    assert(strcmp(r.status, "404 Not Found") == 0);
}

int main(void) {
    RUN(shortens_and_expands_a_url);
    RUN(expanding_an_unknown_code_returns_zero);
    RUN(shorten_never_disturbs_an_existing_entry);
    RUN(http_route_end_to_end_through_handle_request);
    RUN(an_empty_body_shorten_is_a_400);
    RUN(unknown_route_is_404);
    printf("All tests passed.\n");
    return 0;
}

Compile and run with gcc -std=c17 -Wall -Wextra -Wpedantic -pthread -o test_run UrlShortener.c test_UrlShortener.c && ./test_run. The pre-fill test is the interesting one: it directly writes a specific entry into store.entries[0], then calls store_shorten and asserts that entry survives untouched under its own code.

5 The Interface

Verified against a real running server with real curl requests.

INPUTPOST /shortenlong URL as the request body
What it expects
curl -X POST :8081/shorten -d 'https://example.com/a/very/long/path'
OUTPUTGET /{code}307 redirect to the original URL
What it returns
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/a/very/long/path

6 Run It & Automate It

Save the code as UrlShortener.h / UrlShortener.c / main.c and compile it with gcc — 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
gcc -pthread -o shortener main.c UrlShortener.c && ./shortener
Starts listening on http://127.0.0.1:8081.

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
$ ./shortener &
Listening on http://127.0.0.1:8081
$ curl -X POST :8081/shorten -d 'https://example.com/a/very/long/path'
http://127.0.0.1:8081/55w52o
$ curl -D - -o /dev/null :8081/55w52o
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/a/very/long/path
$ curl -o /dev/null -w '%{http_code}\n' :8081/nosuchcode
404
If it breaks — how to fix it
🚨 could not bind to :8081
Something else is already listening on port 8081, possibly the rest-api project if it is still running on a different port — check with a process list.
🚨 curl just hangs.
A GET with no body still needs the server to send Content-Length: 0, which the code above always does — if you modify the response building, make sure that header is never dropped.
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 'gcc --version'
                // compile with strict warnings on
                sh 'gcc -std=c17 -Wall -Wextra -o app *.c -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 click counting. Track how many times each code has been visited. (Teaches: extending the shared state under the same mutex.)
  2. Persist to a file. Save the table to disk like the to-do-list project does, so links survive a restart. (Teaches: combining file persistence with a running server.)
  3. Let the caller choose a custom code. Accept an optional custom slug in the POST body. (Teaches: validating user input against existing keys.)
What you learned
You learned to build a complete small HTTP service from a raw socket up, including a real collision-avoidance loop for generated identifiers using a genuinely secure random source, and the difference a 307 redirect makes over a 301/302. Related: Concurrency in C, Structs and Arrays.