Zoi is an HTTP server template written in Zig that depends only on the standard library. It is not a library — it is a starting point for building your own Zig server. Clone it, edit src/routes.zig, and go.
Most web frameworks are built around the assumption that you should not have to think about the server. Zoi takes the opposite position: the server is simple enough that you should just own it.
Rather than hiding routing, request parsing, and memory management behind a versioned API, Zoi gives you a small, readable codebase that you can read in an afternoon and modify freely. There is no framework to update, no breaking changes to absorb, and no behavior you cannot inspect. When something does not work the way you want, you fix it directly rather than waiting for an upstream maintainer.
The entire implementation — routing, middleware, static files, templating, JSON parsing, cookie handling, and JWT verification — fits in a handful of files totalling a few hundred lines. Every piece of functionality is there because a server will likely need it.
On the technical side, Zig's comptime type system lets the parser accept any struct type at the call site without runtime reflection or code generation. Each request is handled inside an arena allocator that is reset between requests, which means memory management is handled structurally rather than requiring individual frees in handler code. Worker threads are spawned at startup, failed workers are automatically restarted, and the memory and routing overhead per request is minimal.
Zoi has been running in production for over a year. Zoi's site is self-hosted, and benchmarks show Zoi can sustain over 10,000 requests per second against a live SQLite backend on commodity hardware. That is about 3.6x the throughput of an equivalent Bun server on the same machine, achieved through Zig's threading model and the elimination of lock contention via thread-local storage. See the performance writeup for the full breakdown.
The architecture is straightforward under load: a fixed thread pool accepts connections, each worker uses an arena allocator that resets between requests, and the router is a simple linear scan with no heap allocation per match. There is no runtime, no garbage collector, and no framework overhead.
Two things to know before deploying:
- No TLS. Zoi does not terminate HTTPS. Run it behind nginx, Caddy, or any TLS-terminating proxy — the same setup you would use for any backend.
- Zig is pre-1.0. The language and standard library are still evolving. Zig updates almost always require changes to the server code. Because you own the code rather than depending on a versioned package, those changes are yours to make on your own schedule. Zoi tracks the current stable version of Zig.
- Zig 0.16
git clone https://github.com/AndrewGossage/Zoi
cd Zoi
zig build runThe server will start on the address and port configured in config.json.
src/
main.zig — entry point, wires config and routes together
routes.zig — define your routes here
server.zig — core server, router, and parser
config.zig — loads config.json
fmt.zig — template rendering
auth.zig — JWT verification utilities
static/ — static files served by the built-in static handler
config.json — server configuration
config.json controls the server:
{
"address": "127.0.0.1",
"port": "8081",
"workers": 3
}| Field | Description | Default |
|---|---|---|
address |
Bind address | — |
port |
Port to listen on | — |
workers |
Number of worker threads | 1 |
Routes are defined in src/routes.zig as a slice of Route structs.
pub const routes = &[_]server.Route{
.{ .path = "/", .callback = index },
.{ .path = "/static/*", .callback = server.static },
.{ .path = "/:param", .callback = param_test },
.{ .path = "/api/:endpoint", .method = .POST, .callback = postEndpoint },
};:name— matches a single path segment and makes it available viaParser.params*— wildcard, matches the rest of the path (use for static file routes)method— defaults to.GET; set to anystd.http.Methodvalue
Routes can have a middleware chain that runs before the main callback. Middleware receives the same Context and can store values for the handler using c.put.
.{ .path = "/", .middleware = &[_]Callback{ auth_check }, .callback = index }fn auth_check(c: *Context) !void {
try c.put("user", "alice");
}
fn index(c: *Context) !void {
const user = c.get("user").?;
// ...
}server.Parser provides helpers for extracting data from requests.
const Params = struct { id: []const u8 };
const p = try server.Parser.params(Params, c);const Query = struct { value: ?[]const u8 };
const q = server.Parser.query(Query, c.allocator, c.request);const Body = struct { name: []const u8 };
const body = try server.Parser.json(Body, c.allocator, c.request);var cookies = try server.Parser.parseCookies(c.allocator, c.request);
const token = cookies.get("session");const decoded = try server.Parser.urlDecode(raw, c.allocator);// Plain response
try c.request.respond(body, .{ .status = .ok, .keep_alive = false });
// JSON response
const headers = &[_]std.http.Header{
.{ .name = "Content-Type", .value = "application/json" },
};
try server.sendJson(c.allocator, c.request, my_struct, .{
.status = .ok,
.keep_alive = false,
.extra_headers = headers,
});fmt.renderTemplate reads an HTML file and replaces $field$ placeholders with values from an anonymous struct.
const body = try fmt.renderTemplate(c.io, "./static/index.html", .{
.username = "alice",
.title = "Dashboard",
}, c.allocator);
defer c.allocator.free(body);In your HTML:
<h1>Welcome, $username$</h1>
<title>$title$</title>Use server.static as the callback on any route ending with *. Files are served from the current working directory, and dotfiles are blocked by default.
.{ .path = "/static/*", .callback = server.static }A request to /static/styles/main.css will serve static/styles/main.css. If the path has no extension, index.html is appended automatically.
auth.zig provides JWT verification using HMAC-SHA256. Set the JWT_SECRET environment variable and call auth.decodeAuth to verify and decode a token from a cookie value.
const claims = try auth.decodeAuth(c.allocator, token);Thanatos demonstrates using Zoi as a lightweight alternative to Tauri or Electron for desktop applications.