v0.1.0
The first release, published as zfast. Needs Zig 0.16.
Install it pinned — zig fetch --save git+https://github.com/nevindra/zfast?ref=v0.1.0. Without the ?ref= you get whatever main is that day.
What is in it
-
Handlers are ordinary functions. What each argument means is worked out while compiling, by one rule: a pointer is a service, a value is request data. A test calls the function directly — no server, no fake request.
-
Routing — path params, wildcards, groups, plugins. The most specific route wins and duplicates are refused (ADR 0013).
-
Requests — path params, query strings and JSON bodies as structs of your own; bodies too big to hold, read as a stream.
-
HTML forms and file uploads, url-encoded and multipart.
-
Bindings that name the field that broke.
Bound(Form(T)),Bound(T)andBound(Query(T))hand the handler every field that would not bind, by name, with the text that arrived — a 422 listing them is one line, and a page showing the form again with one box marked is a few more (ADR 0036). -
Responses — a status in the type (
Status(201, T)), typed redirects, response headers, and aCtxlayer underneath for full control. -
Cookies, and sessions sealed into one with
XChaCha20Poly1305— no server store, no expiry sweep, nothing added to what an idle connection costs (ADR 0035). -
Middleware as an onion of
Ctxfunctions, and resolved values declared by their type. A group prefix may carry a param —app.group("/orgs/:org")— and middleware scoped to it matches whole segments. -
Request ids and JSON log lines.
logger.with(.{ .format = .json, .request_id = true })writes one JSON object per line and puts anX-Request-Idon every response, adopting the proxy's id when it sent a usable one.c.requestId()reaches the same id from a handler. -
Static files held in memory, gzipped once at startup, with ETags and range requests. A file over
max_file_bytesis not refused but opened per request and sent withsendfile, so a directory with a video in it still starts and the memory figure still holds (ADR 0037). -
A handler can answer with a file.
?nilo.FileBodyserves one out of a directory opened on purpose, with ranges,If-Range, conditional requests andHEADhandled for it — and null still meaning 404. The name is checked a segment at a time, and the path handed to the kernel never comes from a request. -
Streamed responses and server-sent events.
-
WebSocket — handshake, framing, masking, pings, closing handshake. A connection that goes quiet is asked whether it is still there and closed with 1001 if it does not answer; a quiet WebSocket is a working one, so this is a ping rather than a deadline (
.idle_ms, 30 seconds,0waits forever). -
Broadcast —
nilo.Room. Saying something to sockets a handler does not hold. Provide aRoomlike any other service,joinon the way in,defer leaveon the way out, andsayreaches everybody in it:fn chat(c: *nilo.Ctx, room: *nilo.Room) !void { var socket = try c.upgrade(); try room.join(&socket); defer room.leave(&socket); var buf: [16 * 1024]u8 = undefined; while (try socket.receive(&buf)) |message| { try room.say(message.kind, message.data); } }
That loop is the one an echo server writes. A post arriving while a connection is quiet is written out by that connection's own fiber, inside
receive, so a handler never sees one — and one client that stops reading costs that client and nobody else. It adds 4 measured bytes per idle connection, with throughput and p99 unmoved (ADR 0038). -
A generated OpenAPI document, written from the signatures rather than from annotations (ADR 0017).
-
Failure in nilo's own words. Get a handler wrong and compilation stops with a sentence naming your route, your argument and the fix;
refusals/is 56 programs written wrong on purpose that keep it that way (ADR 0027). -
nilo.spawnfor work that is not a request, owned by the server so shutdown counts it (ADR 0029).
A full Room backlog drops the oldest post by default, or the newest if you say so, and room.missed(&socket) says how many were dropped. That amends ADR 0020, which refused to have such a queue at all.
What it holds itself to
One allocation per request and 8,767 bytes per idle connection, both hard invariants held by tests rather than by intent (ADR 0018). Measured numbers and the method behind them are in bench/result/http.md, with eight other servers through the same harness in docs/comparison.md.
What is not in it
- Templates — a refusal rather than a backlog item. nilo is for building APIs and services; rendering pages is not what it is for, and the reasoning is in the roadmap.
- Counters. Requests carry an id and lines can be JSON, but how many requests, at what statuses, and how long is not collected anywhere.
- TLS, and with it HTTP/2 and a gRPC server. This is a refusal rather than a gap — terminate in front (ADR 0028).
- A
recovermiddleware. Zig cannot recover from a panic, so there is nothing to build (ADR 0008). - Compressing a handler's response,
permessage-deflate, and streamed multipart. Static files under the spill threshold are compressed, once, at startup; one above it is sent as it lies on disk.
zfast was a working name, and it changed in 0.2.0.