Low-latency HTTP/1.1 server primitives for Zig with comptime routing, typed captures, and composable middleware.
- Comptime route table via
zhttp.Server(.{ .routes = .{ ... } }). - Typed request captures for headers, query params, and path params (
zhttp.parse.*). - Endpoint-first routes (
pub fn call(comptime rctx, req)). - Composable middleware with compile-time
Infocapture merging. - Built-in middleware: static files, CORS, logging, compression, timeout, ETag, request IDs, Expect handling, and security headers.
- Tight hot path with direct parse/write for HTTP/1.1.
- In-tree examples and benchmark harness.
zig build examples
./zig-out/bin/zhttp-example-basic_server --port=8080
zig build examples-checkMinimal server:
const std = @import("std");
const zhttp = @import("zhttp");
const Hello = struct {
pub const Info: zhttp.router.EndpointInfo = .{
.query = struct {
name: zhttp.parse.Optional(zhttp.parse.String),
},
};
pub fn call(comptime rctx: zhttp.ReqCtx, req: rctx.T()) !rctx.Response([]const u8) {
const name = req.queryParam(.name) orelse "world";
const body = try std.fmt.allocPrint(req.allocator(), "hello {s}\n", .{name});
return zhttp.Res.text(200, body);
}
};
const App = zhttp.Server(.{
.routes = .{
zhttp.get("/hello", Hello),
},
});
pub fn main(init: std.process.Init) !void {
const addr: std.Io.net.IpAddress = .{ .ip4 = std.Io.net.Ip4Address.loopback(8080) };
var server = try App.init(init.gpa, init.io, addr, {});
defer server.deinit();
try server.run();
}Add as a dependency:
zig fetch --save git+https://github.com/SmallThingz/zhttp#mainbuild.zig:
const zhttp_dep = b.dependency("zhttp", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zhttp", zhttp_dep.module("zhttp"));zhttp.Server(.{ ... })accepts.Context,.middlewares,.operations,.routes,.config,.error_handler, and.not_found_handler..error_handleris a writer-based hook for user handler/middleware errors with signaturefn(*Server, *std.Io.Writer, comptime ErrorSet: type, err: ErrorSet) zhttp.router.Action. Server parse/validation errors stay on the built-in bad-request path. If no not-found handler is provided, a built-in404 not foundendpoint is used.- Route helpers:
zhttp.get,post,put,delete,patch,head,options, andzhttp.route(...)each take(pattern, EndpointType). - Endpoint types must expose
pub const Info: zhttp.router.EndpointInfo = .{ ... };andpub fn call(comptime rctx: zhttp.ReqCtx, req: rctx.T()) !rctx.Response(Body). - Supported
Bodytypes are[]const u8,[][]const u8,void, and custom structs that exposepub fn body(self, comptime rctx, req: rctx.TReadOnly(), cw) !void. EndpointInfofields:.headers,.query,.path,.middlewares,.operations.- Optional endpoint upgrade hook:
pub fn upgrade(server, stream, r, w, line, res) void. If present andcallreturns101 Switching Protocols, zhttp writes upgrade response and returnszhttp.router.Action.upgraded; the upgrade hook owns connection lifecycle. - Standard middleware signatures are available at top-level as
zhttp.CorsSignature. - Header capture keys match case-insensitively, and
_in field names matches-in incoming headers. - If
Info.pathis omitted, path params default to strings. - Route patterns support both segment params (
/users/{id}) and trailing named globs (/static/{*path}). - Typed request accessors include
req.header(...),req.queryParam(...),req.paramValue(...),req.middlewareData(...), andreq.middlewareStatic(...). - Request body helpers:
req.bodyAll(max_bytes)reads and caches the full request body for repeated use.req.bodyReader()gives one-way streaming access to the request body.req.gpa()returns the server allocator; memory taken from it must be freed manually.
rctx.TReadOnly()produces the same request surface without middleware overrides. Use it from custom response body writers when you want direct access to the base request methods.
[]const u8usesContent-Length(single contiguous body).[][]const u8usesContent-Length(sum of segments, written via vectored I/O).voidusesContent-Length: 0.- Custom response body structs use
Transfer-Encoding: chunked.
Chunked example shape:
const StreamBody = struct {
pub fn body(_: @This(), comptime rctx: zhttp.ReqCtx, req: rctx.TReadOnly(), cw: *zhttp.response.ChunkedWriter) std.Io.Writer.Error!void {
_ = req;
try cw.writeAll("hello ");
try cw.writeAll("world");
}
};
pub fn call(comptime rctx: zhttp.ReqCtx, req: rctx.T()) !rctx.Response(StreamBody) {
_ = req;
return .{ .status = .ok, .body = .{} };
}zhttp.middleware.Staticzhttp.middleware.Corszhttp.middleware.Originzhttp.middleware.Loggerzhttp.middleware.Compressionzhttp.middleware.Timeoutzhttp.middleware.Etagzhttp.middleware.Expectzhttp.middleware.RequestIdzhttp.middleware.SecurityHeaders
zhttp.operations.Corszhttp.operations.Static
Both built-ins are route-tagged operations. Add operation types to endpoint
Info.operations and also register operation order in Server(.{ .operations = .{...} }).
zhttp.operations.Cors discovers matching middlewares by zhttp.CorsSignature,
and zhttp.operations.Static discovers middlewares that expose operationRoutes().
Custom operation shape:
pub fn operation(comptime opctx: zhttp.operations.OperationCtx, router: opctx.T()) void.
Operations self-filter tagged routes via opctx.filter(router).
See examples/builtin_middlewares.zig for the full built-in stack in one server.
examples/basic_server.zigexamples/middleware.zigexamples/builtin_middlewares.zigexamples/route_static_access.zigexamples/echo_body.zigexamples/fast_plaintext.zigexamples/response_modes.zigexamples/compression_negotiation.zigexamples/custom_operation.zig
Benchmark commands and modes are documented in benchmark/README.md.
Source: benchmark/results/latest.json
Config: host=127.0.0.1 path=/plaintext conns=16 iters=200000 warmup=10000 full_request=true
| Target | req/s | ns/req | relative |
|---|---|---|---|
| zhttp | 650197.85 | 1538.00 | 1.108x vs faf |
| faf | 586923.64 | 1703.80 | 0.903x vs zhttp |
No benchmark transport errors were reported.
Fairness notes: both targets use the same benchmark client settings (host/path/conns/iters/warmup/full_request), and fixed response bytes are discovered twice then pinned per target before timed runs
zig build test
zig build test-flake -- --iterations=100 --jobs=1
zig build test-flake -- --iterations=200 --jobs=1 --retries=5 --test-filter="Server stop"
zig build examples
zig build examples-checktest-flake sweeps deterministic seeds, extracts failing test names, and prints both full-suite and single-test repro commands. It only flags a seed when reruns reproduce the failure. Add --verbose to dump full failing logs.