The Simplicity of Express.js.
The Speed of Native C.
CExpress is a lightweight HTTP/1.1 web framework and server designed with Express.js ergonomics, with one
single-threaded event loop per worker process, running directly on native BSD/macOS kqueue and Linux epoll (io_uring readiness polling opt-in).
π TechEmpower Framework Benchmarks
CExpress implements all six TechEmpower (TFB) test types and passes TFB's own verifier on every one. The runs
below use TechEmpower's harness unmodified: their ./tfb toolset, their Postgres image and data, their
wrk load generator, request mix and concurrency levels. Every competitor is built from TFB's own code
in the same run, including Round 23's database leaders (may-minihttp, xitca-web). The database app sends every SQL
statement with its own Sync, as TFB's rules require.
write| Test | CExpress (req/s) | Best other | Place | Run |
|---|---|---|---|---|
| JSON | 787,754 | actix 738,709 | 1st of 5 | 4 |
| Plaintext (pipelined Γ16) | 4,813,888 | actix 4,412,559 | 1st of 5 | 4 |
| db | 328,331 | xitca-web-barebone 332,258 | 2nd of 8 (98.8%) | 10 |
| Multiple queries, 1 | 329,366 | may-minihttp 328,412 | 1st of 8 | 10 |
| Multiple queries, 5 | 110,799 | xitca-web 104,086 | 1st of 8 (+6.4%) | 10 |
| Multiple queries, 20 | 29,713 | actix-http 29,582 | 1st of 8 (+0.4%) | 10 |
| Fortunes | 310,625 | xitca-web-barebone 329,595 | 4th of 8 (94.2%) | 10 |
| Updates, 1 | 165,029 | actix-http 154,826 | 1st of 8 (+6.6%) | 10 |
| Updates, 20 | 20,641 | xitca-web 20,107 | 1st of 8 (+2.7%) | 10 |
Best req/s over TFB's concurrency levels. Run 10 had eight entries (CExpress, actix-http, axum-pg, h2o, Fiber, may-minihttp, xitca-web, xitca-web-barebone); run 4, the last run that measured JSON and plaintext, had five (CExpress, actix, axum, h2o, Fiber). On db and on multiple queries at 1, the top three (CExpress, may-minihttp, xitca-web-barebone) are within 2.4% and 0.8%, a tie; fortunes is the one test where CExpress trails the Round 23 leaders.
wrk all shared one 10-CPU VM on an
Apple Silicon laptop, where an official TechEmpower round uses three dedicated 28-core Xeon machines on
10 GbE. The method is TFB's own and every framework ran in the same run, so the ranking within a
run is a fair comparison. The absolute numbers are not comparable with TechEmpower's
published rounds. Each configuration is a single run; differences of about 2% are inside noise. TechEmpower
archived the project in March 2026, so these are local runs, not an official listing. Full per-test tables,
setup and the rules check: techEmpV1.md.
π Performance & Stress Benchmarks
CExpress is engineered from the ground up for extreme throughput, deterministic microsecond latency, and tiny
memory consumption. All benchmarks below were executed on one Apple Silicon machine (M3 Pro 12-core, 18 GB RAM),
either on macOS directly or in Docker Desktop's Linux VM where marked, in multi-worker cluster mode
(QUIET=1 WORKERS=4) using wrk over 8 threads.
wrk on the same
machine as the server.
β’ Standalone Engine Peak cards: 21 Sep 2026 (100 and 5,000 connections) and 23 Sep 2026 (1,000 connections, after the switch to one arena and one receive buffer per worker).
β’ Memory tab: 21 Sep 2026 (before that switch) and 23 Sep 2026 (after it).
β’ Ping, SQLite, Static, Churn and Hotpath tabs and the Linux spotlight: 24 Sep 2026, from
scripts/stress_test.sh (macOS, 15 s per row, churn 2 s per row),
scripts/docker_stress_test.sh (Linux epoll in Docker, 10 s per row, 100 and 1,000 connections) and
make bench. The macOS machine was busy during this run (load average 7β8 before the benchmark
started, sysmond at ~76% CPU), and wrk shares its cores, so the macOS
/ping figures are lower than the 21β23 Sep ones. Two shorter re-runs gave 176β180k req/s at 100
connections and 191β193k at 1,000.
β’ Framework comparison chart: an earlier engine revision; no competitor was re-measured since.
The 5,000-connection runs show intermittent
wrk read errors of unidentified cause.
epoll in Docker: scripts/docker_stress_test.sh
The server (4 workers) and wrk (8 threads, built natively for arm64) each run in their own
container on one private Docker network, inside Docker Desktop's Linux VM on the same M3 Pro. Traffic goes
container to container. Each row is one 10 s run. Zero worker crashes; peak memory for master plus 4 workers
was 11.8 MB in total (largest single process 2.6 MB).
/ping, 1,000 connections Β· 6.24 ms avg latency Β· 0 errors (240,019 req/s at 100 connections)GET / (6.5 KB page), 100 connections Β· 2.92 ms avg Β· 694.54 MB/sGET /api/todos (SQLite + JSON), 100 connections Β· 3.70 ms avg Β· POST: 7,891 req/sA keep-alive connection that is waiting for its next request no longer owns a request arena or an input buffer. Each worker has one 64 KiB arena and one 8 KiB receive buffer that every connection shares. A connection only gets its own buffer while a request is half-received, and gives it back as soon as that request is answered. Throughput is unchanged.
wrk /ping (macOS, 21β23 Sep 2026)
Measuring raw CExpress request-response dispatch over persistent keep-alive TCP sockets (fixed 4-byte response, zero database I/O, zero JSON overhead).
wrk read errors
The /ping endpoint measures raw server event-loop throughput and kernel socket multiplexing
with zero disk or database overhead. The response is a pre-formatted 4-byte payload. macOS, scripts/stress_test.sh,
24 Sep 2026, 15 s per row, on a busy machine (see the note above); the 21β23 Sep peak on the same machine was
250β257k req/s.
| Concurrency | Threads | Throughput | Avg Latency | Max Latency | Data Transferred | Stability |
|---|---|---|---|---|---|---|
| 100 conns | 8 | 158,730 req/s | 1.94 ms | 81.44 ms | 19.53 MB/s (295.0 MB read) | β 0 errors |
| 1,000 conns | 8 | 181,403 req/s | 6.01 ms | 139.25 ms | 22.32 MB/s (337.0 MB read) | β 0 errors |
| 5,000 conns | 8 | 172,327 req/s | 36.22 ms | 1.22 s | 21.20 MB/s (320.1 MB read) | 1,344 wrk read errors Β· 0 crashes |
kqueue (macOS/BSD) or
epoll (Linux; io_uring opt-in), one event loop per worker process, requests parsed with
picohttpparser straight out of one 8 KiB receive buffer per worker and served from one 64 KiB arena per worker
(not per connection); responses are written with plain
write() calls.
Real-world CRUD benchmark against an embedded SQLite database in WAL (Write-Ahead Logging) mode with
busy_timeout=5000. Four worker processes concurrently execute reads and writes against the same
shared SQLite database file. macOS, scripts/stress_test.sh, 24 Sep 2026, 15 s per row; reads
run against the 20 seeded rows, before any write phase.
| Benchmark Phase | Concurrency | Throughput | Avg Latency | Max Latency | Bandwidth | Lock Errors |
|---|---|---|---|---|---|---|
| GET /api/todos (Read) | 100 conns | 154,644 req/s | 705.68 Β΅s | 39.03 ms | 388.90 MB/s | β 0 locks |
| GET /api/todos (Read) | 1,000 conns | 169,478 req/s | 6.02 ms | 71.47 ms | 426.21 MB/s | β 0 locks |
| GET /api/todos (Read) | 5,000 conns | 163,515 req/s | 26.29 ms | 446.36 ms | 411.21 MB/s | 994 wrk read errors |
| POST /api/todos (Write) | 100 conns | 20,818 req/s | 96.22 ms | 1.65 s | 5.37 MB/s | β 0 locks |
| POST /api/todos (Write) | 1,000 conns | 19,741 req/s | 121.54 ms | 1.25 s | 5.10 MB/s | 301 wrk read errors |
| POST /api/todos (Write) | 5,000 conns | 19,748 req/s | 306.66 ms | 1.81 s | 5.10 MB/s | 5,283 read errors, 3,008 timeouts |
fork(). About 907,000 POST requests completed across the three write rows with
zero worker crashes. The errors in the last column are reported by wrk on the
client side; at 5,000 connections the average write latency (307 ms) approaches wrk's timeout,
so some requests time out. Reads now run at about the /ping rate, which suggests the load
generator, not SQLite, limits them on this machine.
Serves the responsive single-page Todo application HTML directly from disk via CExpress's bounded streaming
engine (res_send_file). The page is 6,481 bytes. macOS, scripts/stress_test.sh,
24 Sep 2026, 15 s per row.
| Concurrency | Threads | Throughput | Avg Latency | Max Latency | Transfer Rate | Total Read |
|---|---|---|---|---|---|---|
| 100 conns | 8 | 117,244 req/s | 0.91 ms | 44.01 ms | 738.86 MB/s | 10.90 GB |
| 1,000 conns | 8 | 118,832 req/s | 8.39 ms | 59.79 ms | 748.87 MB/s | 10.98 GB |
| 5,000 conns | 8 | 110,533 req/s | 42.18 ms | 366.67 ms | 696.57 MB/s | 10.25 GB (655 read errors) |
sendfile, or in 16 KB chunks where sendfile is unavailable, ensuring RAM consumption stays bounded even when saturation pushes throughput
past 740 megabytes per second.
Benchmarks server performance when clients send Connection: close, requiring a full TCP
handshake, HTTP request parsing, response delivery, and connection teardown on every single transaction.
24 Sep 2026. The macOS rows run for 2 s each, and the script waits for TIME_WAIT sockets to drain
between them (13β19k were left after each run; draining took 31β32 s).
| Benchmark Mode | Concurrency | New Conns / Second | Avg Latency | Max Latency | Notes |
|---|---|---|---|---|---|
| GET /ping (Connection: close) | 100 conns | 35,445 conn/s | 2.06 ms | 88.53 ms | 74,501 connections in 2 s Β· 0 connect errors |
| GET /ping (Connection: close) | 1,000 conns | 23,627 conn/s | 4.82 ms | 93.39 ms | 48,505 connections in 2 s Β· 0 connect errors |
| GET /ping (Connection: close) | 5,000 conns | 22,542 conn/s | 6.14 ms | 39.89 ms | 46,175 connections in 2 s Β· 0 connect errors |
| Linux epoll in Docker | 100 conns | 47,132 conn/s | 3.52 ms | 150.18 ms | 10 s run, container to container |
| Linux epoll in Docker | 1,000 conns | 43,806 conn/s | 10.99 ms | 210.85 ms | 10 s run, container to container |
Connection: close,
client load generators on macOS quickly exhaust the ~16k ephemeral port range because closed sockets linger
in TIME_WAIT for 30β60 seconds. For maximum performance in production, always leverage
persistent HTTP keep-alive.
Resident set size (RSS) was measured with ps across the master and all 4 workers after opening N keep-alive
connections from a script and sending one GET /ping on each (macOS). The 21 Sep column is the
engine before the shared arena and receive buffer; the 23 Sep columns are the current one, measured the same way.
| Execution Phase | Total Cluster RSS, 21 Sep | Total Cluster RSS, 23 Sep | Largest Single Worker, 23 Sep | Memory Scaling Behavior (23 Sep) |
|---|---|---|---|---|
| Idle (boot) | 11.5 MB | 11.3 MB | 2.2 MB | Master + 4 pre-forked workers |
| 100 keep-alive conns | 17.1 MB | 11.8 MB | 2.3 MB | Connections now spread evenly across workers |
| 1,000 keep-alive conns | 38.5 MB | 11.9 MB | 2.3 MB | Growth is within rounding |
| 5,000 keep-alive conns | 133.7 MB | 12.7 MB | 2.5 MB | About 0.3 KB per connection, down from about 25 KB |
/ping throughput did not change. Kernel socket buffers are not part of RSS.
POST row, after about 907,000 inserts. /usr/bin/time -l reported a
maximum RSS of 4.2 MB (4,407,296 bytes) for the largest process, which matches the ps sampler. In
the Linux Docker run the whole cluster stayed between 11.2 and 11.8 MB. Zero worker crashes in either run.
make fuzz mutation fuzz run finished clean. LeakSanitizer is not available on macOS, so no leak check has
been run on this platform.
Measured with make bench (compiled with -O2) on 24 Sep 2026, single run. Gauges the raw CPU cycle cost of the
core framework pipeline (parsing, routing, dispatching, response generation) isolated from operating system
socket I/O.
| Pipeline Operation | Latency per Request | Single-Core Theoretical Capacity | Dynamic Allocations |
|---|---|---|---|
| Minimal GET (2 headers) | 226 ns | ~4,400,000 requests / sec | 0 heap allocs |
| 404 Route Miss | 318 ns | ~3,100,000 requests / sec | 0 heap allocs |
| POST with JSON Body | 244 ns | ~4,100,000 requests / sec | 0 heap allocs |
| Browser GET (10 headers, cookies, query) | 763 ns | ~1,310,000 requests / sec | 0 heap allocs |
| β¦same, handler reads a cookie and a header | 770 ns | ~1,300,000 requests / sec | 0 heap allocs |
| yyjson (20-row list serialization) | 696 ns | Default (libc) allocator in the benchmark | Heap allocations (not the arena) |
sizeof(Request) = 9,792 B,
sizeof(Response) = 10,160 B, sizeof(Connection) = 288 B. Per-request data (request body,
response bytes, yyjson documents) comes from one 64 KiB bump-allocator arena per worker, so the request path needs no malloc() or free() unless a request outgrows the
arena. These figures exclude socket I/O and use a static arena.
# 1. Build release binary
make clean && make demo
# 2. Launch 4-worker cluster in background
cd examples/todo_sqlite && QUIET=1 WORKERS=4 ./cexpress_demo
# 3. In another terminal, run standalone wrk benchmarks on /ping
wrk -t8 -c100 -d15s http://127.0.0.1:8080/ping
wrk -t8 -c1000 -d15s http://127.0.0.1:8080/ping
# 4. Or run the full automated stress battery with memory profiling
./scripts/stress_test.sh
# 5. Same battery against Linux (epoll; CEXPRESS_EVENT_LOOP=io_uring for io_uring) in Docker
./scripts/docker_stress_test.sh
# 6. Pure request-path microbenchmark (no sockets)
make bench
π‘οΈ Reliability, Hardening & Test Matrix
High throughput is meaningless without correctness and rock-solid memory safety. CExpress is rigorously verified across 22 dedicated automated test suites, AddressSanitizer and UndefinedBehaviorSanitizer instrumentation, continuous mutation fuzzing, and automated API documentation audits.
Automated Test Suites (make test)
:id), wildcard capturing
(*), sub-router mounting via app_mount().HttpOnly,
SameSite, Secure, Max-Age), chunked body streaming, trailers.multipart/form-data parser, MIME boundary matching, file
upload fields, and truncated payload resilience.application/x-www-form-urlencoded decoding, URL query string parameter
extraction, and percent-encoding normalization.index.html fallbacks, and
directory traversal defense (blocking ../ and null bytes).kqueue (macOS/BSD) and
epoll / io_uring (Linux) readiness registration and timers.SO_REUSEPORT multi-bind,
automatic worker crash recovery, and graceful drain on SIGTERM.Automated Documentation Parity (scripts/check_docs.sh)
To prevent documentation drift, check_docs.sh (invoked via make check-docs) audits
the entire public C API against the documentation:
lib/*.h is missing from the documentation, or if the documentation references any function not
declared in a header.
Memory Sanitizers & Fuzzing (make SANITIZE=1 test & make fuzz)
Memory safety in C requires rigorous automated verification. The entire suite can be compiled under AddressSanitizer and UndefinedBehaviorSanitizer:
# Run entire test suite under ASan + UBSan
make clean && make SANITIZE=1 test
# Run mutation fuzzer on HTTP parser (1,000,000 iterations)
make fuzz
π¦ Installation & Umbrella Header
CExpress exposes its entire public API via a single umbrella header:
/* Includes routing, response builders, middlewares, HTTP parser, static files, the arena allocator and yyjson */
#include "cexpress.h"
Compile and link directly against the static library libcexpress.a:
gcc-16 -Wall -Wextra -std=c11 -O2 -Ipath/to/lib -o my_app main.c path/to/libcexpress.a # Linux: add -D_GNU_SOURCE and -luring
π Hello World
A minimal server with a single route listening on port 8080:
#include "cexpress.h"
#include <stdio.h>
void handler_home(const Request *req, Response *res) {
(void)req;
res_send(res, "Hello from CExpress!");
}
int main(void) {
App app;
app_init(&app);
app_get(&app, "/", handler_home);
printf("Listening on http://localhost:8080\n");
app_listen(&app, 8080);
app_destroy(&app);
return 0;
}
π§ Routing & HTTP Verbs
Handlers are defined with the signature void (*Handler)(const Request *req, Response *res).
Register handlers for standard HTTP verbs:
app_get(&app, "/users", handler_get_users);
app_post(&app, "/users", handler_create_user);
app_put(&app, "/users/:id", handler_update_user);
app_patch(&app, "/users/:id", handler_patch_user);
app_delete(&app, "/users/:id", handler_delete_user);
β’ HEAD Requests: Executes the matching
GET handler while automatically
suppressing the message body.β’ OPTIONS Requests: Returns
200 OK with the computed Allow
header.β’ 405 Method Not Allowed: Emitted automatically if the route path exists but the verb is not registered.
Route Parameters (:id)
Extract URL path variables with req_get_param:
app_get(&app, "/orgs/:org/repos/:repo", handler_repo);
void handler_repo(const Request *req, Response *res) {
const char *org = req_get_param(req, "org");
const char *repo = req_get_param(req, "repo");
char body[128];
snprintf(body, sizeof(body), "Repository: %s/%s", org, repo);
res_send(res, body);
}
Sub-Routers (app_mount)
Modularize routes into isolated Router instances and mount them under path prefixes (like
Express's app.use('/api', apiRouter)):
Router api_router;
router_init(&api_router);
/* Scoped to /api */
router_get(&api_router, "/health", handler_health);
router_get(&api_router, "/metrics", handler_metrics);
/* Mounts routes to /api/health and /api/metrics */
app_mount(&app, "/api", &api_router);
π‘οΈ Middleware Pipeline
Middleware functions match
void (*Middleware)(const Request *req, Response *res, MiddlewareChain *chain).
Advance with chain_next(chain) or terminate early:
void mw_logger(const Request *req, Response *res, MiddlewareChain *chain) {
/* 1. Pre-handler processing */
chain_next(chain);
/* 2. Post-handler inspection (res->status is finalized) */
printf("%s %s -> %d\n", req->method, req->path, res->status);
}
/* Register app-wide middleware */
app_use(&app, mw_logger);
/* Register prefix-scoped middleware (only runs for /admin/*) */
app_use_prefix(&app, "/admin", mw_admin_auth);
/* Register per-route middleware */
app_post_mw(&app, "/checkout", handler_checkout, (Middleware[]){mw_auth}, 1);
Centralized Error Handling
Trigger centralized error handling with chain_error:
void mw_guard(const Request *req, Response *res, MiddlewareChain *chain) {
if (!is_authorized(req)) {
chain_error(chain, 401, "Invalid or missing API key");
return;
}
chain_next(chain);
}
/* Register custom error handler */
void custom_error_handler(int status, const char *msg, const Request *req, Response *res) {
(void)req;
res_status(res, status);
char json[256];
snprintf(json, sizeof(json), "{\"error\":\"%s\",\"status\":%d}", msg, status);
res_json(res, json);
}
app_use_error(&app, custom_error_handler);
π₯ Request & Response API
Inspect headers, cookies, query strings, and body data:
void handler_inspect(const Request *req, Response *res) {
const char *query_sort = req_get_query(req, "sort");
const char *auth_hdr = req_get_header(req, "Authorization");
const char *session = req_get_cookie(req, "session_id");
/* Set status and custom headers */
res_status(res, 200);
res_set_header(res, "X-Engine", "CExpress");
/* Send JSON */
res_json(res, "{\"status\":\"ok\"}");
}
π Streaming & Chunked Responses
Emit HTTP/1.1 chunked responses incrementally using res_write(), attach RFC 7230 chunked trailers
with res_set_trailer(), and finalize with res_end():
void handler_stream(const Request *req, Response *res) {
(void)req;
res_set_header(res, "Content-Type", "text/plain");
/* Stage trailer (declares Trailer: Server-Timing in headers) */
res_set_trailer(res, "Server-Timing", "app;dur=24.2");
/* Write chunks incrementally */
res_write(res, "Chunk 1\n", 8);
res_write(res, "Chunk 2\n", 8);
/* Emits terminal 0\r\n, trailers, and terminating CRLF */
res_end(res);
}
Bounded File Streaming (res_send_file)
Stream large files directly from the filesystem through the non-blocking event loop (sendfile, or bounded 16KB chunks where it is unavailable).
Memory is bounded and whole files are never buffered into RAM:
void handler_download(const Request *req, Response *res) {
(void)req;
if (res_send_file(res, "application/pdf", "reports/report.pdf") != 0) {
res_status(res, 404);
res_send(res, "Report not found");
}
}
Static File Serving (app_serve_static)
Mount a directory of static assets with traversal protection and MIME type detection:
/* GET /static/app.css -> serves public/app.css */
app_serve_static(&app, "/static", "public");
π Multipart & Form Parsing
Parse multipart/form-data file uploads and application/x-www-form-urlencoded forms:
/* Multipart File Uploads */
const char *content_type = req_get_header(req, "Content-Type");
char boundary[128];
MultipartForm form;
if (content_type == NULL || !multipart_parse_boundary(content_type, boundary, sizeof(boundary)) ||
parse_multipart_body(req->body, (size_t)req->content_length, boundary, &form) < 0) {
res_status(res, 400);
res_send(res, "Bad multipart body");
return;
}
for (int i = 0; i < form.part_count; i++) {
if (form.parts[i].filename[0] != '\0') {
printf("File uploaded: %s (%zu bytes)\n", form.parts[i].filename, form.parts[i].data_len);
}
}
β‘ JSON with yyjson
CExpress vendors yyjson and includes it from cexpress.h.
Give it the shared per-worker arena (res->conn->arena, already a pointer) as its allocator so
documents need no freeing; the string returned by
yyjson_mut_write comes from libc and must be freed:
/* Parse incoming JSON */
yyjson_alc alc = arena_yyjson_alc(res->conn->arena);
yyjson_doc *doc = yyjson_read_opts(req->body, (size_t)req->content_length, 0, &alc, NULL);
if (doc != NULL) {
const char *name = yyjson_get_str(yyjson_obj_get(yyjson_doc_get_root(doc), "name"));
/* name points into the arena: use it before the handler returns */
}
/* Construct and send JSON */
yyjson_mut_doc *out = yyjson_mut_doc_new(&alc);
yyjson_mut_val *obj = yyjson_mut_obj(out);
yyjson_mut_doc_set_root(out, obj);
yyjson_mut_obj_add_str(out, obj, "status", "active");
char *text = yyjson_mut_write(out, 0, NULL); /* libc-malloc'd */
if (text != NULL) { res_json(res, text); free(text); }
π Graceful Shutdown
CExpress intercepts SIGINT and SIGTERM synchronously in the native event loop
(EVFILT_SIGNAL on kqueue, signalfd on Linux):
2. Closes idle keep-alive sockets immediately.
3. Sets
Connection: close on active responses and streams until flushed.4. Arms a 5-second deadline timer to prevent stalled clients from blocking process exit.
5. Returns cleanly to
main() so app_destroy() frees all memory.
π TLS / HTTPS
CExpress is plaintext HTTP/1.1 only. TLS support was removed from the engine on 22 Sep 2026: TLS termination belongs at a gateway or reverse proxy in front of it (nginx, an ALB, a sidecar), not inside a library that only parses HTTP/1.1.
app.config.bind_address = "127.0.0.1" (the demo:
BIND_ADDRESS=127.0.0.1) so the plaintext port cannot be reached around the proxy. The default listens
on every IPv4 interface.
π API Cheat Sheet
| Function | Header | Description |
|---|---|---|
app_init(App *app) |
router.h |
Initializes server app and dynamic connection table. |
app_listen(App *app, int port) |
connection.h |
Starts event loop (delegates to cluster if workers > 1). |
app_listen_cluster(App *app, port, n) |
connection.h |
Launches multi-process cluster of n workers (SO_REUSEPORT). |
cluster_listen(App *app, port, n) |
cluster.h |
Cluster supervisor managing worker processes and crash recovery. |
app_destroy(App *app) |
connection.h |
Closes sockets and frees connection tables cleanly. |
app_get / app_post / app_put ... |
router.h |
Registers route handlers for specific HTTP methods. |
app_mount(App *app, prefix, Router *r) |
router.h |
Mounts a sub-router under a URL prefix. |
app_serve_static(App *app, pfx, root) |
router.h |
Mounts static file directory with traversal protection. |
app_use(App *app, Middleware mw) |
router.h |
Registers an app-wide middleware. |
app_use_prefix(App *app, pfx, mw) |
router.h |
Registers prefix-scoped middleware (e.g. /api). |
chain_next(MiddlewareChain *c) |
middleware.h |
Advances to next middleware or route handler. |
chain_error(c, status, message) |
middleware.h |
Delegates to centralized error handler. |
res_status(Response *res, int status) |
response.h |
Sets HTTP status code (200, 201, 404, etc.). |
res_set_header(res, name, value) |
response.h |
Sets a custom response header. |
res_send(Response *res, const char *b) |
response.h |
Sends text/plain body. |
res_json(Response *res, const char *b) |
response.h |
Sends application/json body. |
res_write(Response *res, data, len) |
response.h |
Emits incremental chunk in chunked response. |
res_set_trailer(res, name, value) |
response.h |
Stages RFC 7230 trailer for chunked transfer. |
res_end(Response *res) |
response.h |
Finalizes chunked response and emits trailers. |
res_send_file(res, type, filepath) |
response.h |
Streams a file through the event loop (sendfile, or bounded 16KB chunks). |
req_get_param(req, name) |
http_parser.h |
Retrieves extracted route path variable. |
req_get_query(req, name) |
http_parser.h |
Retrieves parsed query-string parameter. |
req_get_header(req, name) |
http_parser.h |
Looks up request header (case-insensitive). |
req_get_cookie(req, name) |
http_parser.h |
Extracts cookie value from Cookie header. |