Contents
HTTP demos
A tour of the web-app patterns you can build with the `contrib/http` stack. Each demo is one examples/http_*.mere file plus a small static HTML frontend.
All demos share the same build recipe:
./_build/default/bin/mere.exe -w examples/<name>.mere > /tmp/<name>.wat
wat2wasm --enable-tail-call /tmp/<name>.wat -o /tmp/<name>.wasm
node scripts/run_http_server.js /tmp/<name>.wasm
# open http://localhost:8080/ in a browser
The Node runner (scripts/run_http_server.js) provides the extern imports (fs, crypto, http, curl). Every demo listens on port 8080, so run one at a time. The outbound-only http_client_auth demo has no server component; it also runs cleanly under the plain scripts/run_wasm.js runner (they share the same http_fetch env).
The router API
The routing table is a declarative route_entry list fed to router routes not_found. Three entry constructors:
route "GET" "/" home_h // exact
route_pattern "GET" "/post/:slug" post_h // :captures
route_prefix "/admin" admin_subtable // mount subtree
- `route` — exact-path entry. Handler is
str -> str(raw req → body). - `route_pattern` — segments starting with
:capture one URL
segment each. Handler is str list -> str -> str (captures in source order, then req). Arity must match: /post/:slug matches /post/hello but not /post/hello/extra.
- `route_prefix` — nests a whole
route_entry listunder a URL
prefix. Inner entries are declared relative to the mount point ("/" = mount root, "/login" = ). If the inner table has no match, dispatch falls through to the next outer entry.
Query strings are stripped before matching (/search?q=hi matches /search), and the raw request line is still passed to handlers that need to peek at headers or the query.
Full API + rationale in `contrib/http/router.mere`. The `http_router_demo` example exercises all three variants (including two-capture /user/:name/pet/:pet).
The catalog
| demo | pattern | line count |
|---|---|---|
| todo_app | cookie session + KV persistence + rate-limited login | 328 |
| feed_reader | outbound HTTP + RSS/Atom parsing + per-user cache | 350 |
| webhook_receiver | HMAC-SHA256 signature verify + Slack forward | 250 |
| ci_dashboard | workflow_run event ingest + status aggregation | 240 |
| link_shortener | dynamic route via not_found (regex-validated code) | 260 |
| mini_blog | markdown render + RSS 2.0 producer (round-trip with feed_reader) | 320 |
| wiki | anonymous-edit wiki + append-only revision history | 380 |
| chat | Server-Sent Events broadcast + long-lived HTTP | 140 |
| jwt_api | HS256 bearer auth (stateless — tokens survive restart) | 240 |
| file_upload | multipart/form-data parser + byte-identical download | 260 |
| rest_notes | REST verbs (PUT/PATCH/DELETE) + ETag / If-Match concurrency | 300 |
| csv_export | chunked Transfer-Encoding streaming | 110 |
| blog | markdown blog on Postgres — route_prefix "/admin" + route_pattern "/post/:slug" end-to-end | 560 |
| client_auth | outbound HTTP — http_fetch_h, Bearer auth, per-call timeout, response header read | 80 |
| gh_stars | CLI — GitHub repo star count via http_fetch_h (optional GITHUB_TOKEN Bearer) + rate-limit header echo | 110 |
| metrics_demo | Prometheus-style /metrics endpoint — counters + gauges + with_metrics middleware. /metrics gated by with_basic_auth "metrics" "scraper" "s3cret" (Prometheus / Grafana Agent speak Basic Auth natively). Via contrib/http/metrics + contrib/http/basic_auth | 75 |
| pubsub_chat | multi-instance SSE chat — POST publishes to Redis, JS-side subscriber bridges into sse_broadcast so listeners on any instance see every message (verified: 2 instances on :8080 + :8081, POST → both SSE streams) | 100 |
| csrf_demo | Synchronizer-token CSRF on a form POST — session-bound token, csrf_hidden_input in the form, csrf_validate guard on /update. Missing/wrong token → 403, correct → 303 | 90 |
| cache_demo | Cache-Control postures — /asset immutable / /login no-store / /page ETag + If-None-Match 304 round-trip. Via contrib/http/cache | 65 |
| admin_dash | Integration dogfood — one admin console exercises router route_prefix + session + csrf + basic_auth + metrics (with_metrics middleware) + cache + redis_lock. Multi-instance race verified: instance A wins the 500 ms job, B gets 409 "contended" | 200 |
| ws_chat | RFC 6455 WebSocket hub — clients connect to /ws/chat, glue auto-relays messages between peers. POST /announce calls ws_broadcast for server-side pushes. Verified: A→B via native WebSocket, /announce reaches both | 90 |
todo_app
Signup / login / logout with sha256-hashed passwords. Per-user persistent todos stored in a log-structured KV. Cookie session, brute-force throttle on the login endpoint (contrib/http/ratelimit). The whole contrib/http stack layered in one file: access_log → security_headers → cors → static → router.
POST /api/signup—{user, pass}POST /api/login— setssession=cookieGET /api/todos,POST /api/todos,POST /api/todos/toggle,POST /api/todos/delete
Source: examples/http_todo_app.mere
feed_reader
The only demo that actively uses contrib/http/client.mere's outbound http_fetch. Users subscribe to feed URLs; a refresh call fetches each with curl (spawnSync), parses via contrib/xml + contrib/feed, and caches entries per user.
POST /api/feeds{url},POST /api/feeds/refreshGET /api/entries— cached entries across all subscriptions
Verified against real feeds: news.ycombinator.com/rss, blog.rust-lang.org/feed.xml. 40+ entries survive server restart.
Source: examples/http_feed_reader.mere
webhook_receiver
GitHub-style signed webhook ingress. POST /webhooks/github reads the X-Hub-Signature-256 header and verifies HMAC-SHA256 over the raw body. Passing events go to an audit log and optionally forward to a configured Slack URL.
POST /webhooks/github— signed eventsGET /audit— recent deliveriesPOST /config/forward— set outbound target
Verified: unsigned / wrong-sig → 401, valid push/pull_request/issues events → 200 + forwarded downstream.
Source: examples/http_webhook_receiver.mere
ci_dashboard
Same ingress pattern as webhook_receiver but focused on GitHub workflow_run events. Extracts nested JSON fields (via a small scan-based finder) and maintains a per-(repo, branch, workflow) status board.
POST /webhooks/github— signed workflow_runGET /api/jobs,GET /— auto-refresh HTML dashboard
Source: examples/http_ci_dashboard.mere
link_shortener
First demo with a dynamic route: GET / is not in the fixed route table; the not_found fallback validates the code as [A-Za-z0-9_-]+, looks it up in KV, and returns a 302 redirect while incrementing a per-code hit counter.
POST /api/shorten—{url, code?}GET /— 302POST /api/links/delete
Source: examples/http_link_shortener.mere
mini_blog
Producer side of the XML round-trip. Admin login + POST creates markdown posts; /feed.xml emits RSS 2.0 that contrib/feed (the consumer side used by feed_reader) parses cleanly.
- Admin:
POST /api/login,POST /api/posts - Public:
GET /,GET /post/,GET /feed.xml
Source: examples/http_mini_blog.mere
wiki
Anonymous-edit wiki. Every save is a new page/ key in KV — nothing is ever overwritten, so the past state is always retrievable and the "history" page is just a filtered key listing.
POST /api/pages{slug, title, body, author}GET /page/,GET /page/,/v/ GET /page//history
Source: examples/http_wiki.mere
chat
Long-lived HTTP + SSE broadcast. GET /sse/chat is intercepted at the Node glue layer (contrib/http/http.glue.js) and held open; a POST to /api/messages fans out via sse_broadcast to every open EventSource.
Two curl -N /sse/chat clients receive all events from a third process in matching order — verified.
POST /api/messages,GET /sse/chat,GET /api/messages
Source: examples/http_chat.mere
jwt_api
Stateless bearer auth via HS256 JWTs. No server-side session table — the token payload itself carries the user claim. Signing secret is persisted in KV so restarts don't invalidate outstanding tokens.
POST /api/login→{ token }GET /api/me,GET /api/tasks,POST /api/tasks— all withAuthorization: Bearer
Verified: tampered signature → 401, forged payload claiming sub=admin → 401, restart preserves tokens.
Source: examples/http_jwt_api.mere
file_upload
multipart/form-data parser written in pure Mere (contrib/http/multipart.mere). Uploaded files land in KV and are served back via GET /files/ with the right Content-Type + Content-Disposition. Text files round-trip byte-identically.
POST /api/upload— form-data withfilefieldGET /files/— inline download
Source: examples/http_file_upload.mere
rest_notes
Proper REST verbs (PUT / PATCH / DELETE) + ETag optimistic concurrency. Each response carries ETag: "v; every mutation requires If-Match: to match the server's current value or returns 412 Precondition Failed. Two clients editing the same note can't blindly clobber each other.
POST /api/notes,GET/PUT/PATCH/DELETE /api/notes/
Status codes matrix: 201/200/204/400/404/405/412/428.
Source: examples/http_rest_notes.mere
csv_export
Pair to chat's SSE: chat pushes small events to many subscribers, this pushes one large payload to one client. Both hijack the default "buffer then end" response cycle; the CSV handler calls http_stream_start + repeated http_stream_write to emit rows one at a time via chunked transfer encoding.
GET /api/logs.csv?rows=N— streamed CSV
100 000 rows / 4.3 MB in ~100 ms on the demo machine, with no full- body allocation on the Mere heap.
Source: examples/http_csv_export.mere
Shared contrib
Everything the demos import is under `contrib/`:
contrib/http/— 22 modules includingrouter(exact /:capture/ prefix),client(outbound curl-based fetch with request + response headers, per-call timeout),metrics(Prometheus-style counters + gauges + auto-counting middleware),session(in-memory cookie session store — random 16-hex ids,HttpOnly; SameSite=Laxdefaults),basic_auth(RFC 7617 middleware),csrf(synchronizer-token guard on top of session),cache(Cache-Control postures + ETag/If-None-Match 304),websocket(RFC 6455 hub — server-side broadcast + client auto-relay),json_body,escape,cookie,security,access_log,cors,static,multipart,sse,stream, and the Node gluecontrib/kv/— log-structured KV + pipe-separated pack/unpackcontrib/xml/,contrib/feed/,contrib/markdown/,contrib/json/— parsers / rendererscontrib/auth/jwt.mere— HS256 sign / verifycontrib/webhook/github.mere— GitHub signed webhook helperscontrib/log/log.mere— JSON lines logger
The refactoring sweep that extracted these has cut ~350 lines of duplicated boilerplate out of the demos — a new HTTP handler typically opens with 10 imports + 20 lines of business logic.