Release v0.10.0
ZAP Release v0.10.0
Updates
What's new in ZAP?
- Upgraded to Zig 0.14!
- Breaking Changes for
zap.Endpoint
- New
zap.App
! - Updated README
- Contributions!
After a break, I'm about to work a lot more with Zap, and in preparation made a few improvements which might also work in favor of newcomers.
BTW newcomers: please, also check out these other, pure-zig (which Zap is not) HTTP server projects:
- http.zig : Pure Zig! Close to Zap's model. Performance = good!
- jetzig : Comfortably develop modern web applications quickly, using http.zig under the hood
- zzz : Super promising, super-fast, especially for IO-heavy tasks, io_uring support - need I say more?
I can't wait for the day that Zap becomes obsolete. It would be a very good sign for the Zig HTTP server space!
Breaking Changes for zap.Endpoint
These breaking changes are meant to be improvements.
- no
@fieldParentPtr
: Endpoints now directly get their@This()
pointer passed into their methods - request handlers are allowed to return errors!
- the
.error_strategy
decides if errors are logged to console or reported as HTML to the client (for debugging in the browser) - no "Settings":
path
anderror_strategy
are required for Endpoints- all http method handlers must be present, but of course may be empty
- all of the above are checked at comptime, with meaningful compile error messages
- you register your custom Endpoint instances directly with the
zap.Endpoint.Listener
, no need to provide an.endpoint()
method.
It's best illustrated by example of error.zig
(of the updated endpoints
example) which creates the ErrorEndpoint
:
//!
//! An ErrorEndpoint
//!
const std = @import("std");
const zap = @import("zap");
/// A simple endpoint listening on the /error route that causes an error on GET
/// requests, which gets logged to the response (=browser) by default
pub const ErrorEndpoint = @This();
path: []const u8 = "/error",
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
pub fn get(_: *ErrorEndpoint, _: zap.Request) !void {
return error.@"Oh-no!";
}
// unused:
pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn put(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn delete(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn patch(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn options(_: *ErrorEndpoint, _: zap.Request) !void {}
All relevant examples have been updated accordingly.
The New zap.App
In a way, zap.App
takes the zap.Endpoint
concept one step further: instead of having only per-endpoint instance data (fields of your Endpoint struct), endpoints in a zap.App
easily share a global 'App Context'.
In addition to the global App Context, all Endpoint request handlers also receive an arena allocator for easy, care-free allocations. There is one arena allocator per thread, and arenas are reset after each request.
Just like regular / legacy zap.Endpoints
, returning errors from request handlers is OK. It's decided on a per-endpoint basis how errors are dealt with, via the ErrorStrategy
enum field.
Here is a complete zap.App
example:
//!
//! Part of the Zap examples.
//!
//! Build me with `zig build app_basic`.
//! Run me with `zig build run-app_basic`.
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const zap = @import("zap");
// The global Application Context
const MyContext = struct {
db_connection: []const u8,
pub fn init(connection: []const u8) MyContext {
return .{
.db_connection = connection,
};
}
};
// A very simple endpoint handling only GET requests
const SimpleEndpoint = struct {
// zap.App.Endpoint Interface part
path: []const u8,
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
// data specific for this endpoint
some_data: []const u8,
pub fn init(path: []const u8, data: []const u8) SimpleEndpoint {
return .{
.path = path,
.some_data = data,
};
}
// handle GET requests
pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void {
const thread_id = std.Thread.getCurrentId();
r.setStatus(.ok);
// look, we use the arena allocator here -> no need to free the response_text later!
// and we also just `try` it, not worrying about errors
const response_text = try std.fmt.allocPrint(
arena,
\\Hello!
\\context.db_connection: {s}
\\endpoint.data: {s}
\\arena: {}
\\thread_id: {}
\\
,
.{ context.db_connection, e.some_data, arena.ptr, thread_id },
);
try r.sendBody(response_text);
std.time.sleep(std.time.ns_per_ms * 300);
}
// empty stubs for all other request methods
pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
const StopEndpoint = struct {
path: []const u8,
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
pub fn get(_: *StopEndpoint, _: Allocator, context: *MyContext, _: zap.Request) !void {
std.debug.print(
\\Before I stop, let me dump the app context:
\\db_connection='{s}'
\\
\\
, .{context.*.db_connection});
zap.stop();
}
pub fn post(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn put(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn delete(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn patch(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn options(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
pub fn main() !void {
// setup allocations
var gpa: std.heap.GeneralPurposeAllocator(.{
// just to be explicit
.thread_safe = true,
}) = .{};
defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
const allocator = gpa.allocator();
// create an app context
var my_context = MyContext.init("db connection established!");
// create an App instance
const App = zap.App.Create(MyContext);
var app = try App.init(allocator, &my_context, .{});
defer app.deinit();
// create the endpoints
var my_endpoint = SimpleEndpoint.init("/test", "some endpoint specific data");
var stop_endpoint: StopEndpoint = .{ .path = "/stop" };
//
// register the endpoints with the app
try app.register(&my_endpoint);
try app.register(&stop_endpoint);
// listen on the network
try app.listen(.{
.interface = "0.0.0.0",
.port = 3000,
});
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
std.debug.print(
\\ Try me via:
\\ curl http://localhost:3000/test
\\ Stop me via:
\\ curl http://localhost:3000/stop
\\
, .{});
// start worker threads -- only 1 process!!!
zap.start(.{
.threads = 2,
.workers = 1,
});
}
Updated README
- restructured the examples section a bit
- got rid of all the microbenchmark stuff
- shout-outs to great Zap alternatives (http.zig, Jetzig, zzz)
Contributions!
Special thanks to:
- Victor Moin (vctrmn): Fix deprecated warning in facil.io #154
- Joshua B. (OsakiTsukiko): updated .gitignore, Endpoint improvements
- Thom Dickso 448D n (cosmicboots): Add type checking to simple_router's handle_func #125
What's coming up...?
I am contemplating upgrading the underlying facil.io library to the new and improved version 0.8!
Thanks for reading and helping out 😊!
Using it
In your zig project folder (where build.zig
is located), run:
zig fetch --save "git+https://github.com/zigzap/zap#v0.10.0"
Then, in your build.zig
's build
function, add the following before
b.installArtifact(exe)
:
const zap = b.dependency("zap", .{
.target = target,
.optimize = optimize,
.openssl = false, // set to true to enable TLS support
});
exe.root_module.addImport("zap", zap.module("zap"));