C4 C 4 Punk Developers

CWIST guide · Beginner · 12 min read

Building a web service with CWIST

Install the library, serve your first route, add JSON, a database and a health endpoint, then run the binary you just produced.

What you will build

A single statically linked binary that answers HTTP on port 8080, serves one HTML route and one JSON route, reads from an embedded SQLite database, and exposes metrics and a health check. No runtime is installed alongside it and no reverse proxy is required for it to start.

What you will need

  • A C compiler with C17 support (GCC or Clang).
  • make, git and about ten minutes.
  • Linux, macOS or a BSD. CWIST vendors BoringSSL, lsquic, libttak and SQLite3, so there is no dependency list to satisfy first.

Step 1: install CWIST

Build from source:

git clone https://github.com/c4punks/CWIST.git
cd CWIST
make
sudo make install   # optional; PREFIX=/opt/cwist to relocate

Or install the released build through Homebrew, on macOS or Linuxbrew:

brew install c4punks/cwist/cwist

Either path gives you libcwist.a, the public headers, the cwist CLI and cwist.pc pkg-config metadata.

Step 2: the smallest server that works

Create main.c:

#include <cwist/app.h>

static void hello(cwist_http_request *req, cwist_http_response *res) {
    (void)req;
    cwist_sstring_assign(res->body, "Hello from CWIST!");
}

int main(void) {
    cwist_app *app = cwist_app_create();
    cwist_app_get(app, "/", hello);
    cwist_app_listen(app, 8080);
    cwist_app_destroy(app);
    return 0;
}

Three things are worth naming here. cwist_app_create() allocates the application and everything hanging off it. cwist_app_get() registers a handler for a method and path. cwist_app_listen() blocks and runs the accept loop, so anything you want configured must be configured before it.

Step 3: compile and run

gcc -o server main.c -lcwist -lssl -lcrypto -lz -lzstd \
    -lbrotlienc -lbrotlicommon -luriparser -lcjson -ldl -lpthread -lm
./server

If you installed with pkg-config metadata available, gcc -o server main.c $(pkg-config --cflags --libs cwist) does the same job. Then, from another terminal:

curl -i http://localhost:8080/

You should see HTTP/1.1 200 OK and the greeting. If the connection is refused, the process is not listening yet or port 8080 is already taken. Change the argument to cwist_app_listen() and rebuild.

Step 4: path parameters and query strings

A route segment beginning with a colon becomes a path parameter, and both parameter maps are read the same way:

static void greet(cwist_http_request *req, cwist_http_response *res) {
    const char *name  = cwist_query_map_get(req->path_params, "name");
    const char *style = cwist_query_map_get(req->query_params, "style");

    char out[256];
    snprintf(out, sizeof(out), "%s, %s",
             style ? style : "Hello", name ? name : "stranger");
    cwist_sstring_assign(res->body, out);
}

cwist_app_get(app, "/greet/:name", greet);

Both getters return NULL when the key is absent, so check before you dereference. /greet/ada?style=Hi now answers Hi, ada.

Step 5: answer with JSON

CWIST links cJSON, so building a response body is ordinary cJSON work:

#include <cjson/cJSON.h>

static void status(cwist_http_request *req, cwist_http_response *res) {
    (void)req;
    cJSON *doc = cJSON_CreateObject();
    cJSON_AddStringToObject(doc, "service", "demo");
    cJSON_AddBoolToObject(doc, "ready", 1);

    char *text = cJSON_PrintUnformatted(doc);
    cwist_http_header_add(&res->headers, "Content-Type",
                          "application/json; charset=utf-8");
    cwist_sstring_assign(res->body, text);

    cJSON_free(text);
    cJSON_Delete(doc);
}

Note the two frees. cJSON_PrintUnformatted() hands you an allocation, and the document itself is separate; cwist_sstring_assign() copies, so both can go once the body is set.

Step 6: add a database and the operational endpoints

Everything below is configuration on the app, and all of it has to happen before cwist_app_listen():

int main(void) {
    cwist_app *app = cwist_app_create();

    cwist_app_use_db(app, ":memory:");   /* or a file path */
    cwist_app_use_pqc_layer(app, true);  /* hybrid PQ TLS */
    cwist_app_enable_metrics(app);
    cwist_app_enable_healthz(app);
    cwist_app_auto_rdbms(app, 5432);     /* local PostgreSQL, if present */

    cwist_app_get(app, "/", hello);
    cwist_app_get(app, "/status", status);

    cwist_app_listen(app, 8080);
    cwist_app_destroy(app);
    return 0;
}

Use :memory: while you are iterating and a file path when you want the data to survive a restart. cwist_app_auto_rdbms() looks for a PostgreSQL, MySQL or MariaDB on localhost and wires it up if it finds one; if there is nothing there, the call is a no-op rather than an error.

Step 7: query the database

#include <cwist/core/db/sql.h>

cwist_db *db = NULL;
cwist_db_open(&db, "app.sqlite");
cwist_db_exec(db, "CREATE TABLE IF NOT EXISTS notes ("
                  "id INTEGER PRIMARY KEY, body TEXT)");

cJSON *rows = NULL;
cwist_error_t err = cwist_db_query(db, "SELECT id, body FROM notes", &rows);
if (err.error.err_i16 == 0 && rows) {
    /* rows is a cJSON array of objects keyed by column name */
    cJSON_Delete(rows);
}

Results come back as a cJSON array, one object per row keyed by column name, which means the same value can be serialised straight into a response or walked in C. You own the array; delete it when you are done.

Step 8: choose a server mode before you measure

The reactor is the default and takes the throughput. The classic thread-per-connection pool answers the median and p99 faster on our published runs. Switch with an environment variable:

CWIST_C1M_MODE=0 ./server   # classic thread-per-connection pool
./server                    # reactor (default)

Measure both against your own workload before deciding. The separate guide on server modes explains what each one is optimising and how to read the distribution.

Summary

You have a single binary that routes, speaks JSON, reads a database and exposes health and metrics, with no runtime deployed beside it. From here, the WASI guide compiles this same source to wasm32-wasip2, and the libttak guide covers the memory model underneath.