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
Two operations, one shared table: shorten a URL into a code, and expand a code back into its 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.
#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);
}
}
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.
store_shorten does.301/302 redirect for every case — browsers and some HTTP clients are allowed to change a POST into a GET when following those codes.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.
#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.
What it expects
curl -X POST :8081/shorten -d 'https://example.com/a/very/long/path'What it returns
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/a/very/long/path6 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.
gcc -pthread -o shortener main.c UrlShortener.c && ./shortenerStarts 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.
$ ./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
404Content-Length: 0, which the code above always does — if you modify the response building, make sure that header is never dropped.// 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.' }
}
}
- Add click counting. Track how many times each code has been visited. (Teaches: extending the shared state under the same mutex.)
- 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.)
- Let the caller choose a custom code. Accept an optional custom slug in the POST body. (Teaches: validating user input against existing keys.)
307 redirect makes over a 301/302. Related: Concurrency in C, Structs and Arrays.