Mesh is a lightweight HTTP server library for Zig, built on top of zap, with a tree-based router, middleware support, JSON helpers, and optional periodic database saving via DDB.
Requirements:
- Zig version:
0.17.0 - OS:
- Linux
- macOS
- Tree-based router - exact and dynamic (
:param) segments, exact matches preferred. - Method handlers - multiple HTTP methods per route node.
- Middlewares - a simple chain with early exit.
- Context - per-request allocator, DB handle, and up to 4 captured route params.
- DB abstraction -
DBcan wrap a real DDB, a no-op VoidDB, or nothing at all. - JSON helpers - parse request bodies or slices into Zig types.
- Response helpers - quick error, not-found, and body senders.
- Background saver - optional worker thread that periodically calls
db.save().
const std = @import("std");
const Mesh = @import("Mesh");
const zap = Mesh.zap;
const router = @import("router.zig");
const response = @import("response.zig");
fn hello(r: zap.Request, ctx: router.Context) void {
response.sendBody(r, "Hello from Mesh!");
}
const routes = router.RouteNode{
.segment = "",
.children = &.{
.{
.segment = "hello",
.handlers = &.{
.{ .method = .GET, .handler = hello },
},
},
},
};
pub fn main(init: std.process.Init) !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
var server = Mesh.Server{
.db = .empty(),
.allocator = gpa.allocator(),
.root = &routes,
.port = 8080,
.io = init.io,
};
defer server.stop();
try server.start();
}RouteNode describes your tree. An empty segment is the root. A node with param = "name" matches any segment and captures it.
const routes = router.RouteNode{
.segment = "",
.children = &.{
.{
.segment = "users",
.children = &.{
.{
.segment = "me",
.handlers = &.{ .{ .method = .GET, .handler = getMe } },
},
.{
.segment = ":id",
.param = "id",
.handlers = &.{
.{ .method = .GET, .handler = getUser },
.{ .method = .DELETE, .handler = delUser },
},
},
},
},
},
};Lookup is done with router.findRoute(path, &routes), returning a Found with handlers and captured params. Exact children take priority over dynamic ones.
A middleware is a function fn (r: zap.Request, ctx: router.Context) bool. Returning false stops the chain - assume the response has already been sent.
fn auth(r: zap.Request, ctx: router.Context) bool {
if (r.getHeader("X-Token") == null) {
response.sendError(r, .unauthorized, "Missing token");
return false;
}
return true;
}Attach them via Server.middlewares:
var server = Mesh.Server{
// ...
.middlewares = &.{ auth },
};db.DB is a small union-like handle:
| Constructor | Behavior |
|---|---|
DB.fromDDB(p) |
Wraps a real ddb.DB. save() works. |
DB.fromVoid(p) |
No-op. save() does nothing. |
DB.empty() |
No DB at all. save() does nothing. |
Use db.asDdb() to fetch the real handle, or db.isReal() to check.
If a real DB is provided and a worker thread starts successfully, Server saves it roughly every 5 seconds.
json.parseFromSlice and json.parseFromBody decode JSON into a Zig type. They accept any allocator - an arena is recommended.
const User = struct { id: u32, name: []const u8 };
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const user = try json.parseFromSlice(User, arena.allocator(), body_bytes);For requests, parseFromBody sends a proper error response on failure and returns null.
Helpers in response.zig:
sendBody(r, msg)- 200 OK with body.sendError(r, status, msg)- generic status + message.sendErrorPrint(r, status, allocator, fmt, args)- formatted error.sendBadRequest(r, msg)- 400.sendInternalServerError(r, msg)- 500.notFound(r, allocator, path)- 404 with the path.getParamStrSave(r, allocator, name)- param lookup with error reporting.
| Field | Default | Description |
|---|---|---|
port |
8080 |
Listening port. |
threads |
4 |
Worker threads for zap. |
log |
true |
Enable zap request logging. |
middlewares |
&.{} |
Middleware chain. |
db |
- | DB handle. |
io |
- | std.Io for the background worker. |
start() blocks (it calls zap.start). Call stop() from another thread or after the listener returns to shut down the worker and clear static state.
main branch:
zig fetch --save https://github.com/Neon32eeee/mesh/archive/refs/heads/main.tar.gz
build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "myproject",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const mesh = b.dependency("mesh", .{
.target = target,
.optimize = optimize,
});
const mesh_module = b.createModule(.{
.root_source_file = mesh.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("Mesh", mesh_module);
b.installArtifact(exe);
}- Run main
zig build run
- Run tests
zig build test
All examples live under examples/ and can be run individually:
- No DB version
zig build run-no_db_version
- DB version
zig build run-db_version
- JSON
zig build run-json
- Params
zig build run-params
- Response
zig build run-response
- Middleware
zig build run-middleware
Or run all examples at once:
zig build example
More details at the Wiki.