1 The Problem
We want a REST API: a web service that lets clients create, read, update, and delete records (a to-do list, say) using standard HTTP methods (GET, POST, PUT, DELETE) and JSON. It teaches the conventions of REST — how the entire web of apps and services talks to itself.
2 How to Think About It
A REST API is a routing table plus shared state. Build the state and the routing logic first, entirely independent of sockets, then wire sockets on last.
Store (create/list/delete) guarded by a pthread_mutex_t. → 2. Write handle_request as pure routing logic: method + path + body in, a status + JSON body out. → 3. Parse a raw HTTP request off the socket by hand, including Content-Length. → 4. Spawn one pthread per connection so the server handles requests concurrently.
3 The Build — explained part by part
Here is the complete API. C has no web framework in its standard library at all — nothing like Java’s built-in HttpServer, let alone a real framework — so this project does by hand exactly what a framework automates: request parsing, routing, JSON encoding, and thread safety.
#ifndef REST_API_H
#define REST_API_H
#include <pthread.h>
#define MAX_TODOS 256
#define MAX_TASK 128
#define MAX_BODY 1024
typedef struct {
int id;
char task[MAX_TASK];
int done;
} Todo;
/* All the mutable state behind one mutex, since this server handles each
* connection on its own OS thread (see serve_all below) -- without the
* lock, two requests arriving at once could corrupt `items` or hand out a
* duplicate id. */
typedef struct {
Todo items[MAX_TODOS];
int count;
int next_id;
pthread_mutex_t lock;
} Store;
void store_init(Store *store);
/* Appends a new, not-done todo with the next never-reused id, holding the
* lock for the whole operation. Returns the created todo. */
Todo store_create(Store *store, const char *task);
/* Copies up to `cap` todos (already in ascending-id order) into `out`.
* Returns how many were copied. */
int store_list(const Store *store, Todo *out, int cap);
/* Removes the todo with the given id. Returns 1 if found, 0 otherwise. */
int store_delete(Store *store, int id);
typedef struct {
char status[32];
char body[MAX_BODY];
} Response;
/* Minimal JSON encoding, escaping only quotes and backslashes -- enough for
* this project's plain-text task names. A real project reaches for a JSON
* library; C's standard library has none, and this build environment
* cannot reach a package registry to fetch one (the same constraint Rust's
* and Java's versions of this project hit with serde_json/Jackson). */
void todo_to_json(const Todo *t, char *out, int out_cap);
void todos_to_json(const Todo *items, int count, char *out, int out_cap);
/* Just enough of a JSON object parser for this API's one request shape:
* {"task": "some text"}. Returns 1 if a "task" field was found. */
int parse_task_field(const char *body, char *out, int out_cap);
/* The whole route table for this tiny API, 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
#include "RestApi.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void store_init(Store *store) {
store->count = 0;
store->next_id = 1;
pthread_mutex_init(&store->lock, NULL);
}
Todo store_create(Store *store, const char *task) {
pthread_mutex_lock(&store->lock);
Todo t;
t.id = store->next_id++;
strncpy(t.task, task, sizeof(t.task) - 1);
t.task[sizeof(t.task) - 1] = '\0';
t.done = 0;
store->items[store->count++] = t;
pthread_mutex_unlock(&store->lock);
return t;
}
int store_list(const Store *store, Todo *out, int cap) {
int n = store->count < cap ? store->count : cap;
memcpy(out, store->items, sizeof(Todo) * (size_t)n);
return n;
}
int store_delete(Store *store, int id) {
pthread_mutex_lock(&store->lock);
for (int i = 0; i < store->count; i++) {
if (store->items[i].id == id) {
for (int j = i; j < store->count - 1; j++) store->items[j] = store->items[j + 1];
store->count--;
pthread_mutex_unlock(&store->lock);
return 1;
}
}
pthread_mutex_unlock(&store->lock);
return 0;
}
static void escape_json(const char *s, char *out, int out_cap) {
int o = 0;
for (int i = 0; s[i] != '\0' && o < out_cap - 2; i++) {
if (s[i] == '\\' || s[i] == '"') out[o++] = '\\';
out[o++] = s[i];
}
out[o] = '\0';
}
void todo_to_json(const Todo *t, char *out, int out_cap) {
char escaped[MAX_TASK * 2];
escape_json(t->task, escaped, sizeof(escaped));
snprintf(out, (size_t)out_cap, "{\"id\":%d,\"task\":\"%s\",\"done\":%s}",
t->id, escaped, t->done ? "true" : "false");
}
void todos_to_json(const Todo *items, int count, char *out, int out_cap) {
out[0] = '['; out[1] = '\0';
char one[MAX_TASK * 2 + 64];
for (int i = 0; i < count; i++) {
todo_to_json(&items[i], one, sizeof(one));
if (i > 0) strncat(out, ",", (size_t)(out_cap - (int)strlen(out) - 1));
strncat(out, one, (size_t)(out_cap - (int)strlen(out) - 1));
}
strncat(out, "]", (size_t)(out_cap - (int)strlen(out) - 1));
}
int parse_task_field(const char *body, char *out, int out_cap) {
const char *key = strstr(body, "\"task\"");
if (!key) return 0;
const char *after_key = key + strlen("\"task\"");
const char *colon = strchr(after_key, ':');
if (!colon) return 0;
const char *p = colon + 1;
while (*p == ' ' || *p == '\t') p++;
if (*p != '"') return 0;
p++;
int o = 0;
while (*p != '\0' && o < out_cap - 1) {
if (*p == '"') { out[o] = '\0'; return 1; }
if (*p == '\\' && *(p + 1) != '\0') { p++; out[o++] = *p; p++; continue; }
out[o++] = *p;
p++;
}
return 0; /* ran off the end without a closing quote */
}
static int is_valid_id(const char *s) {
if (*s == '\0') return 0;
for (const char *p = s; *p; p++) if (!isdigit((unsigned char)*p)) return 0;
return 1;
}
Response handle_request(Store *store, const char *method, const char *path, const char *body) {
Response r;
if (strcmp(method, "GET") == 0 && strcmp(path, "/todos") == 0) {
Todo items[MAX_TODOS];
int n = store_list(store, items, MAX_TODOS);
strcpy(r.status, "200 OK");
todos_to_json(items, n, r.body, sizeof(r.body));
return r;
}
if (strcmp(method, "POST") == 0 && strcmp(path, "/todos") == 0) {
char task[MAX_TASK];
if (parse_task_field(body, task, sizeof(task))) {
Todo t = store_create(store, task);
strcpy(r.status, "201 Created");
todo_to_json(&t, r.body, sizeof(r.body));
} else {
strcpy(r.status, "400 Bad Request");
strcpy(r.body, "{\"error\":\"missing \\\"task\\\" field\"}");
}
return r;
}
if (strcmp(method, "DELETE") == 0 && strncmp(path, "/todos/", 7) == 0) {
const char *id_str = path + 7;
if (!is_valid_id(id_str)) {
strcpy(r.status, "400 Bad Request");
strcpy(r.body, "{\"error\":\"invalid id\"}");
return r;
}
int id = atoi(id_str);
if (store_delete(store, id)) {
strcpy(r.status, "204 No Content");
r.body[0] = '\0';
} else {
strcpy(r.status, "404 Not Found");
strcpy(r.body, "{\"error\":\"no such todo\"}");
}
return r;
}
strcpy(r.status, "404 Not Found");
strcpy(r.body, "{\"error\":\"no such route\"}");
return r;
}
#define _POSIX_C_SOURCE 200809L
#include "RestApi.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 8080
#define MAX_REQUEST 8192
/* Parses the request line and, if present, a Content-Length body, from a
* raw HTTP/1.1 request read straight off the socket. A real project would
* use a framework (unreachable here, see RestApi.h); this is what that
* framework is doing underneath. */
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_BODY];
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_BODY + 256];
snprintf(out, sizeof(out),
"HTTP/1.1 %s\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n\r\n%s",
resp.status, strlen(resp.body), resp.body);
send(args->fd, out, strlen(out), 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 :8080");
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);
}
}
pthread_mutex_lock anywhere that touches items compiles cleanly and just corrupts data under real concurrent load.Response handle_request(Store *store, const char *method, const char *path, const char *body) — the entire route table, written as a plain function that takes a method, path and body and returns a status and JSON body. It knows nothing about sockets, which is exactly why the tests below can call it directly with a fake request and no real network involved.
todo_to_json / parse_task_field — hand-rolled JSON encoding and a narrow, one-shape decoder, scanning character by character for the closing quote while unescaping
\" and \\. C has no JSON library in its standard library and this sandbox cannot reach a package registry to fetch one.pthread_create(&thread, NULL, serve_connection, args); pthread_detach(thread); — C has no async runtime built in (that is what libraries like
libuv provide), so this server gets its concurrency the plain way: a real OS thread per connection. pthread_detach tells the runtime to reclaim the thread’s resources automatically when it finishes, since nothing ever calls pthread_join on it.
store->items from serve_connection without going through a function that locks the mutex first — unlike Rust, this compiles fine and only breaks under real concurrent load, which makes it easy to miss in casual testing.store_create/store_list/store_delete, which always lock internally, as this project does.Content-Length bytes for the body — this client sends Connection: close, but a browser or another client might keep the connection open, and then the read would hang forever.Content-Length from the headers and recv exactly that many more bytes, as read_request does.4 Test & Prove Each Part
We test the routing logic directly — no real socket involved — by calling handle_request with fake methods, paths and bodies.
#include "RestApi.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define RUN(name) do { name(); printf("PASS: %s\n", #name); } while (0)
static void creates_and_lists_todos(void) {
Store store;
store_init(&store);
Response created = handle_request(&store, "POST", "/todos", "{\"task\":\"Buy milk\"}");
assert(strcmp(created.status, "201 Created") == 0);
assert(strstr(created.body, "\"task\":\"Buy milk\"") != NULL);
Response listed = handle_request(&store, "GET", "/todos", "");
assert(strcmp(listed.status, "200 OK") == 0);
assert(strstr(listed.body, "Buy milk") != NULL);
}
static void deletes_a_todo_and_404s_on_repeat(void) {
Store store;
store_init(&store);
handle_request(&store, "POST", "/todos", "{\"task\":\"one\"}");
Response deleted = handle_request(&store, "DELETE", "/todos/1", "");
assert(strcmp(deleted.status, "204 No Content") == 0);
Response deleted_again = handle_request(&store, "DELETE", "/todos/1", "");
assert(strcmp(deleted_again.status, "404 Not Found") == 0);
}
static void rejects_a_post_with_no_task_field(void) {
Store store;
store_init(&store);
Response result = handle_request(&store, "POST", "/todos", "{}");
assert(strcmp(result.status, "400 Bad Request") == 0);
}
static void unknown_route_is_404(void) {
Store store;
store_init(&store);
Response result = handle_request(&store, "GET", "/nope", "");
assert(strcmp(result.status, "404 Not Found") == 0);
}
static void parses_a_task_field_with_an_escaped_quote(void) {
char out[128];
int ok = parse_task_field("{\"task\":\"say \\\"hi\\\"\"}", out, sizeof(out));
assert(ok);
assert(strcmp(out, "say \"hi\"") == 0);
}
static void deleting_an_invalid_id_is_a_400_not_a_crash(void) {
Store store;
store_init(&store);
Response result = handle_request(&store, "DELETE", "/todos/not-a-number", "");
assert(strcmp(result.status, "400 Bad Request") == 0);
}
static void ids_are_never_reused_after_a_delete(void) {
Store store;
store_init(&store);
handle_request(&store, "POST", "/todos", "{\"task\":\"first\"}");
handle_request(&store, "DELETE", "/todos/1", "");
Response created = handle_request(&store, "POST", "/todos", "{\"task\":\"second\"}");
assert(strstr(created.body, "\"id\":2") != NULL); /* not id 1 again */
}
static void list_is_ordered_by_id(void) {
Store store;
store_init(&store);
handle_request(&store, "POST", "/todos", "{\"task\":\"a\"}");
handle_request(&store, "POST", "/todos", "{\"task\":\"b\"}");
Response listed = handle_request(&store, "GET", "/todos", "");
const char *pos_a = strstr(listed.body, "\"a\"");
const char *pos_b = strstr(listed.body, "\"b\"");
assert(pos_a != NULL && pos_b != NULL && pos_a < pos_b);
}
int main(void) {
RUN(creates_and_lists_todos);
RUN(deletes_a_todo_and_404s_on_repeat);
RUN(rejects_a_post_with_no_task_field);
RUN(unknown_route_is_404);
RUN(parses_a_task_field_with_an_escaped_quote);
RUN(deleting_an_invalid_id_is_a_400_not_a_crash);
RUN(ids_are_never_reused_after_a_delete);
RUN(list_is_ordered_by_id);
printf("All tests passed.\n");
return 0;
}
Compile and run with gcc -std=c17 -Wall -Wextra -Wpedantic -pthread -o test_run RestApi.c test_RestApi.c && ./test_run. Because handle_request takes plain values in and returns a plain Response, every test here runs with zero sockets and zero timing concerns.
5 The Interface
Verified against a real running server with real curl requests, not just the unit tests above.
What it expects
curl -X POST :8080/todos -d '{"task":"Buy milk"}'What it returns
[{"id":1,"task":"Buy milk","done":false}]6 Run It & Automate It
Save the code as RestApi.h / RestApi.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 server main.c RestApi.c && ./serverStarts listening on
http://127.0.0.1:8080. Try it with curl in another terminal.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.
$ ./server &
Listening on http://127.0.0.1:8080
$ curl -X POST :8080/todos -d '{"task":"Buy milk"}'
{"id":1,"task":"Buy milk","done":false}
$ curl -X POST :8080/todos -d '{"task":"Write report"}'
{"id":2,"task":"Write report","done":false}
$ curl :8080/todos
[{"id":1,"task":"Buy milk","done":false},{"id":2,"task":"Write report","done":false}]
$ curl -X DELETE :8080/todos/1 -o /dev/null -w '%{http_code}\n'
204
$ curl :8080/todos
[{"id":2,"task":"Write report","done":false}]
$ curl -X DELETE :8080/todos/1 -o /dev/null -w '%{http_code}\n'
404PORT in main.c and your curl commands."task" key — the parser here only looks for the literal text "task":"...".// 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 a PATCH /todos/{id} route to toggle
done. (Teaches: extending the route match.) - Use a real JSON library such as
cJSONorjson-cif you have network access, and compare how much hand-rolled parsing disappears. (Teaches: what a small, focused dependency buys you.) - Add request logging. Print method, path and status for every request. (Teaches: simple middleware-style wrapping.)
curl requests. Related: Concurrency in C, Structs and Arrays.