← thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · C++ Project

REST API

A tiny REST API for a todo list, built from a listening socket up: no framework, because C++’s standard library, like C’s, ships nothing resembling an HTTP server.

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

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.

Where this shows up: every mobile app backend, every single-page web app, every microservice, every public API (Stripe, Twitter, GitHub). REST is the lingua franca of the internet. Understanding it is essential to backend work.

2 How to Think About It

Every incoming connection gets handled on its own thread, which means the shared todo list needs real protection against two requests arriving at once. That protection is the one part of this project most worth reading carefully.

The plan — in plain English
1. Listen on a socket, accepting connections in a loop. → 2. Hand each connection to its own std::thread, detached. → 3. Parse the HTTP request (method, path, and a Content-Length-aware body). → 4. Route it through handle_request, which is the only part touching the shared Store. → 5. Write the JSON response back and close the connection.

GET

POST

PUT

DELETE

Request arrives

Which method?

Return records as JSON

Create a record

Update a record

Remove a record

Send JSON response

3 The Build — explained part by part

Here is the complete API. The route table (handle_request) is deliberately kept free of any socket code, so it can be tested by calling it directly — every test below does exactly that, with no network involved at all.

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

struct Todo {
    int id;
    std::string task;
    bool done;
};

// All the mutable state behind one std::mutex, since this server handles
// each connection on its own thread (see main.cpp) -- without the lock,
// two requests arriving at once could corrupt the item list or hand out a
// duplicate id. Locking happens through std::lock_guard inside each
// method, not by hand: even if a line between lock and unlock were to
// throw, the mutex still unlocks itself when the guard goes out of scope,
// unlike C's pthread_mutex_lock/pthread_mutex_unlock pair, which needs
// every exit path to remember to unlock.
class Store {
public:
    // Appends a new, not-done todo with the next never-reused id.
    Todo create(const std::string &task);

    // Returns all todos, already in ascending-id order.
    std::vector<Todo> list() const;

    // Removes the todo with the given id. Returns true if found.
    bool remove(int id);

private:
    mutable std::mutex mutex_;
    std::vector<Todo> items_;
    int next_id_ = 1;
};

// 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; like C, this build environment cannot reach a package
// registry to fetch one (the same constraint this project's C version hit
// with no equivalent of serde_json or Jackson available either).
std::string todo_to_json(const Todo &t);
std::string todos_to_json(const std::vector<Todo> &items);

// Just enough of a JSON object parser for this API's one request shape:
// {"task": "some text"}. Returns std::nullopt if no "task" field was found.
std::optional<std::string> parse_task_field(const std::string &body);

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

// 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 std::string &method,
                         const std::string &path, const std::string &body);

#include "RestApi.hpp"
#include <algorithm>
#include <sstream>

Todo Store::create(const std::string &task) {
    std::lock_guard<std::mutex> lock(mutex_);
    Todo t{next_id_++, task, false};
    items_.push_back(t);
    return t;
}

std::vector<Todo> Store::list() const {
    std::lock_guard<std::mutex> lock(mutex_);
    return items_; // a copy, so the caller can use it after the lock is released
}

bool Store::remove(int id) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto it = std::find_if(items_.begin(), items_.end(),
                            [id](const Todo &t) { return t.id == id; });
    if (it == items_.end()) return false;
    items_.erase(it);
    return true;
}

static std::string json_escape(const std::string &s) {
    std::string out;
    out.reserve(s.size());
    for (char c : s) {
        if (c == '"' || c == '\\') out += '\\';
        out += c;
    }
    return out;
}

std::string todo_to_json(const Todo &t) {
    std::ostringstream out;
    out << "{\"id\":" << t.id << ",\"task\":\"" << json_escape(t.task)
        << "\",\"done\":" << (t.done ? "true" : "false") << "}";
    return out.str();
}

std::string todos_to_json(const std::vector<Todo> &items) {
    std::ostringstream out;
    out << "[";
    for (size_t i = 0; i < items.size(); i++) {
        if (i > 0) out << ",";
        out << todo_to_json(items[i]);
    }
    out << "]";
    return out.str();
}

std::optional<std::string> parse_task_field(const std::string &body) {
    auto key = body.find("\"task\"");
    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 == "GET" && path == "/todos") {
        return Response{"200 OK", todos_to_json(store.list())};
    }
    if (method == "POST" && path == "/todos") {
        auto task = parse_task_field(body);
        if (!task) return Response{"400 Bad Request", "{\"error\":\"missing task field\"}"};
        Todo created = store.create(*task);
        return Response{"201 Created", todo_to_json(created)};
    }
    if (method == "DELETE" && path.rfind("/todos/", 0) == 0) {
        std::string id_str = path.substr(std::string("/todos/").size());
        try {
            int id = std::stoi(id_str);
            if (store.remove(id)) return Response{"204 No Content", ""};
            return Response{"404 Not Found", "{\"error\":\"no such todo\"}"};
        } catch (const std::exception &) {
            return Response{"400 Bad Request", "{\"error\":\"invalid id\"}"};
        }
    }
    return Response{"404 Not Found", "{\"error\":\"no such route\"}"};
}

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

// Reads a full HTTP/1.1 request off `fd`: headers first, then the body if
// Content-Length says there is one. Loops recv() until the blank-line
// boundary and, if needed, the full body have arrived -- a single recv()
// call is not guaranteed to return the whole request at once.
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"
        << "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;
        // A lambda captures what the thread needs by value, instead of C's
        // pthread_create, which needs a raw function pointer plus a void*
        // argument the callback has to cast back to the right type by hand.
        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
std::lock_guard<std::mutex> lock(mutex_); at the top of every Store method — RAII locking. The mutex unlocks itself automatically when lock goes out of scope, on every return path, even one caused by an exception. C’s version of this project locks and unlocks a pthread_mutex_t by hand at the start and end of every function, which means every exit path (including an early return the original author might add later) has to remember to unlock — miss one, and every other thread deadlocks waiting for a lock that will never be released.

std::thread(serve_connection, client_fd, &store).detach(); — a type-safe replacement for pthread_create, which needs a raw function pointer and a void* argument the callback has to cast back to the right type by hand. Here the compiler checks the argument types at the call site itself.

Response handle_request(Store &store, ...) — the entire route table as one pure-ish function taking plain strings in and returning a plain Response struct out, with no socket code anywhere inside it. This is what makes 9 of this project’s 9 tests possible without a single real network call.

read_request in main.cpp — loops recv() until the \r\n\r\n header boundary arrives, then keeps reading until Content-Length bytes of body have arrived too, because a single recv() call is never guaranteed to return an entire HTTP request at once.
Common mistakes — and how to avoid them
✗ Returning a reference or pointer into Store’s internal vector from list() — another thread could resize or mutate that vector while the caller is still reading from it, a data race even with the mutex, because the lock is released the moment list() returns.
✓ Return a copy of the data, as this project’s list() does, so the caller has its own safe snapshot once the lock is released.
✗ Assuming one recv() call returns the whole HTTP request — TCP is a byte stream with no message boundaries, so a large request (or a slow connection) can easily arrive in several pieces.
✓ Loop until you have seen the full header boundary and, per Content-Length, the full body, as read_request does here.

4 Test & Prove Each Part

Nine checks, from the Store’s id-management and thread-safety-relevant copy semantics, through JSON encoding and a hand-written request-body parser, to the full route table’s behaviour for GET, POST, and DELETE — all without a single real socket.

Store::create assigns permanent, never-reused ids
Store::list returns todos in ascending-id order
Store::remove finds a todo by id and reports success or failure correctly
todo_to_json escapes quotes and backslashes in the task text
parse_task_field extracts the task string from a JSON body
parse_task_field returns std::nullopt when the field is missing
handle_request GET /todos lists everything currently stored
handle_request POST /todos creates a new todo
handle_request DELETE removes a todo, then 404s on a repeat delete of the same id
C++test_RestApi.cpp
#include "RestApi.hpp"
#include <cassert>
#include <iostream>

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

static void store_create_assigns_never_reused_ids() {
    Store store;
    Todo a = store.create("first");
    Todo b = store.create("second");
    assert(a.id == 1);
    assert(b.id == 2);
    assert(!a.done);
}

static void store_list_returns_todos_in_ascending_id_order() {
    Store store;
    store.create("first");
    store.create("second");
    auto items = store.list();
    assert(items.size() == 2);
    assert(items[0].id == 1 && items[1].id == 2);
}

static void store_remove_finds_by_id_and_reports_success() {
    Store store;
    Todo a = store.create("first");
    store.create("second");
    assert(store.remove(a.id));
    assert(store.list().size() == 1);
    assert(!store.remove(999));
}

static void todo_to_json_escapes_quotes_and_backslashes() {
    Todo t{1, "say \"hi\" \\ bye", false};
    std::string json = todo_to_json(t);
    assert(json.find("\\\"hi\\\"") != std::string::npos);
    assert(json.find("\\\\") != std::string::npos);
}

static void parse_task_field_extracts_the_task_string() {
    auto task = parse_task_field(R"({"task": "buy milk"})");
    assert(task.has_value());
    assert(*task == "buy milk");
}

static void parse_task_field_returns_nullopt_when_missing() {
    assert(!parse_task_field(R"({"other": "value"})").has_value());
}

static void handle_request_get_todos_lists_everything() {
    Store store;
    store.create("first");
    Response r = handle_request(store, "GET", "/todos", "");
    assert(r.status == "200 OK");
    assert(r.body.find("first") != std::string::npos);
}

static void handle_request_post_creates_a_todo() {
    Store store;
    Response r = handle_request(store, "POST", "/todos", R"({"task": "new task"})");
    assert(r.status == "201 Created");
    assert(r.body.find("new task") != std::string::npos);
    assert(store.list().size() == 1);
}

static void handle_request_delete_removes_then_404s_on_repeat() {
    Store store;
    Todo created = store.create("to remove");
    Response first = handle_request(store, "DELETE", "/todos/" + std::to_string(created.id), "");
    assert(first.status == "204 No Content");
    Response second = handle_request(store, "DELETE", "/todos/" + std::to_string(created.id), "");
    assert(second.status == "404 Not Found");
}

int main() {
    RUN(store_create_assigns_never_reused_ids);
    RUN(store_list_returns_todos_in_ascending_id_order);
    RUN(store_remove_finds_by_id_and_reports_success);
    RUN(todo_to_json_escapes_quotes_and_backslashes);
    RUN(parse_task_field_extracts_the_task_string);
    RUN(parse_task_field_returns_nullopt_when_missing);
    RUN(handle_request_get_todos_lists_everything);
    RUN(handle_request_post_creates_a_todo);
    RUN(handle_request_delete_removes_then_404s_on_repeat);
    std::cout << "All tests passed.\n";
    return 0;
}

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

5 The Interface

Even a tiny hand-rolled API has a real interface. Here it is, documented the same way a professional would describe any HTTP service.

INPUTPOST /todosa JSON body like {"task": "..."}
What it expects
{"task": "buy milk"}
OUTPUTOUTPUTthe created todo, the full list, or a 204/404 on delete
What it returns
{"id":1,"task":"buy milk","done":false}

6 Run It & Automate It

Save the code as RestApi.hpp / RestApi.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 restapi main.cpp RestApi.cpp && ./restapi 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/todos -d '{"task": "buy milk"}'
{"id":1,"task":"buy milk","done":false}
$ curl http://127.0.0.1:8080/todos
[{"id":1,"task":"buy milk","done":false}]
$ curl -X DELETE http://127.0.0.1:8080/todos/1 -w '\nHTTP:%{http_code}\n'

HTTP:204
$ curl -X DELETE http://127.0.0.1:8080/todos/1 -w '\nHTTP:%{http_code}\n'
{"error":"no such todo"}
HTTP:404
If it breaks — how to fix it
🚨 curl: (56) Recv failure: Connection reset by peer
Almost always means the server thread crashed or returned before writing a response — check read_request is not blocking forever waiting for a Content-Length that was never actually sent.
🚨 {"error":"missing task field"}
The exact key "task" with a colon and a quoted string value must appear in the body — parse_task_field does a literal text search, not real JSON parsing, so anything else (a different key name, unquoted value) will not match.
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 a PATCH /todos/<id> route to mark a todo done. (Teaches: extending the route table and the Store together.)
  2. Add a thread pool instead of a thread per connection. (Teaches: why unbounded thread creation is a real resource risk under load, and how a fixed pool of worker threads pulling from a queue avoids it.)
  3. Return proper HTTP error codes for malformed JSON, not just a missing field. (Teaches: how much a real JSON parser actually validates that a hand-written substring search does not.)
What you learned
You learned that RAII locking with std::lock_guard is exception-safe in a way manual pthread_mutex lock/unlock calls are not, that std::thread with a lambda is a type-safe replacement for pthread_create’s void* callback, and why Store::list must return a copy rather than a reference into thread-shared state. Related: Concurrency in C++, Classes and RAII.