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,gitand 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 relocateOr install the released build through Homebrew, on macOS or Linuxbrew:
brew install c4punks/cwist/cwistEither 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
./serverIf 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.