Capi the capybara

CAPI

CAPI is A Plain Interface.

A minimal HTTP/1.1 web API written in C. The goal here is simplicity and little dependencies.

CAPI

Dependencies


Quick start

1. Build the build tool (once)

forge is the project's build driver. Build it once with:

gcc -o forge forge.c

and just call forge whenever you want to compile Capi.

Important

forge shells out to:

gcc -o build/capi -g -Wall -Wextra -Icore/src -Ilibs api/api.c -pthread

2. Run the server

Run from the repository root — the / route reads ./frontend/index.html:

./build/capi

Open http://localhost:6969 or call the endpoints with curl.


Routes

All routes are hard-coded in the block of handle_client() in api/api.c.

GET /

Serves frontend/index.html as text/html. If the file cannot be read, returns 404.

curl http://localhost:6969/

GET /capi.svg

Serves frontend/capi.svg as image/svg+xml. It is the capybara mascot shown on the index page and used as the favicon.

curl http://localhost:6969/capi.svg

GET /hasUrlParam/<name>

It is an example of a generic GET endpoint.

Reads the value of the paramValue query parameter. If the query string has no paramValue, the route falls back to a JSON body with a paramValue string property (useful for GET requests sent with a body).

curl "http://localhost:6969/hasUrlParam/anything?paramValue=hello"

or:

curl -X GET http://localhost:6969/hasUrlParam/anything \
     -H 'Content-Type: application/json' \
     -d '{"paramValue":"hello"}'

POST /hasBodyParam

It is an example of a generic POST endpoint.

Reads the bodyParam string property from the JSON request body and echoes it back as the response body.

curl -X POST http://localhost:6969/hasBodyParam \
     -H 'Content-Type: application/json' \
     -d '{"bodyParam":"hello"}'

Adding a route

A new route is just another branch.

Example for a GET /health endpoint:

case VERB_GET:
    if (strcmp(res.resource, "/") == 0) {
        /* serve a file */
    }
    else if (strcmp(res.resource, "/health") == 0) {
        char *body = "{\"status\":\"ok\"}";
        send_200_to_client_json(client, body, (int)strlen(body));
    }
    else {
        send_404_to_client(client);
    }
    break;

Key points:

Per-request scratch memory

Each request gets its own Arena. The arena ships inside ahb_lib.h, so it is compiled together with AHB_LIB_IMPLEMENTATION:

Arena arena = {0};
arena_init(&arena);

Response res = read_from_client_arena(&arena, client, payload, PAYLOAD_SIZE);
char *page = ahb_read_entire_file_arena(&arena, "./frontend/index.html");
char *copy = arena_strdup(&arena, res.resource);

/* ... */

arena_free(&arena);

Client jobs

main() hands each accepted socket to a detached thread through a ClientJob struct. Extend that struct to pass request-scoped context into handle_client():

typedef struct {
    int client;
    char *example;
    /* add more per-connection data here */
} ClientJob;

Shared state

Handlers run concurrently, so any shared mutable state must be guarded. A mutex is already sketched in the file:

static pthread_mutex_t your_lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&your_lock);
/* read/write shared state */
pthread_mutex_unlock(&your_lock);

Libraries

The headers in libs/ are STB-style single-header libraries: declarations are always visible, and the implementation is compiled only in the translation unit that defines the matching macro before including the header.

HeaderMacroOverview
ahb_lib.hAHB_LIB_IMPLEMENTATIONGeneral utilities: logging, command execution, args, string builders, hash map, arena bump allocator, file helpers, temp allocator, colors
json.hJSON_IMPLEMENTATIONMinimal JSON parser with typed object/array accessors
network.hNETWORK_IMPLEMENTATIONLoopback TCP server/client, HTTP request parsing and response helpers

Define each macro in exactly one translation unit; api.c does this for all three. Note that network.h also defines AHB_LIB_IMPLEMENTATION, so a file including it must not include ahb_lib.h with its implementation again.


Request lifecycle

sequenceDiagram
    participant C as Client
    participant M as main()
    participant T as handle_client()

    C->>M: TCP connect
    M->>M: accept_client() + timeout setup
    M->>T: pthread_create(detached) with ClientJob
    T->>T: arena_init()
    T->>T: read_from_client_arena()
    T->>T: route on verb + resource
    T->>C: send_200/400/404/500
    T->>T: arena_free()
    T->>C: close()

Projects that uses CAPI

https://fleabag-app.duckdns.org


Limitations

These are intentional for a teaching-sized framework:

Do not expose this server to a network without adding proper request limits, timeouts and TLS in front of it.

Troubleshooting

Could not load the file index.html — the server was not started from the repository root. cd to the project root and run ./build/capi.

Could not bind the server to port 6969 — another process is using the port:

ss -ltnp | grep 6969

Rebuild after editing headers — forge recompiles api/api.c, which is the only translation unit with the library implementations, so a single ./forge api is enough.