Zap

Description

Zap is a micro web framework that is mainly used to write REST APIs.

Zap isn't really a fully Zig project, it just wraps and leverages the power of facil.io, which implies two things:

  • All the heavy lifting is done in C, so we can't give much of the performances merits to Zig on this apart from not adding heavy overhead.
  • From the programmer point of view it seems like your project is 100% Zig, so you have no C overhead, except having to link libc.

Note that the exemple of code below is taken from the exemples from the Zap GitHub repository in order to best explain the basics of this web framework.

The framework works by defining callbacks that are going to be called when a certain event happens. For example, you can define a callback that is going to be called when a request is received.

Here you define the callback

  var listener = zap.Endpoint.Listener.init(
      allocator,
      .{
          .port = 3000,
          .on_request = on_request, // HERE
          .log = true,
          .public_folder = "examples/endpoint/html",
          .max_clients = 100000,
          .max_body_size = 100 * 1024 * 1024,
      },
  );

And then it is going to call the following function every time the endpoint is called. We can qualify this function as an handler.

  fn on_request(r: zap.Request) void {
      if (r.path) |the_path| {
          std.debug.print("REQUESTED PATH: {s}\n", .{the_path});
      }
  
      r.sendBody("<html><body><h1>Hello from ZAP!!!</h1></body></html>") catch return;
  }
facil.io

Since Zap is just a wrapper around facil.io, they have very similar performances. We can clearly see that with the benchmarks. We can also notice that zap seems slightly better than facil.io, but it is not the case in reality both are equivalent this is due to the randomness explained here that can come from benchmarks like this. Note that I did add a few other web servers in order to get the comparison more meaningful.

/HEIG_ZIG/images/facilio.png

Basic example

Getting a very simple Zap server up and running is quite straightforward. Example taken from one of the examples.

  const std = @import("std");
  const zap = @import("zap");
  
  fn on_request(r: zap.Request) void {
      if (r.path) |path| {
          std.debug.print("PATH: {s}\n", .{path});
      }
  
      if (r.query) |query| {
          std.debug.print("QUERY: {s}\n", .{query});
      }
  
      r.sendBody("Hello from zap :)") catch return;
  }
  
  pub fn main() !void {
      // Create listener
      var listener = zap.HttpListener.init(.{
          .port = 3000,
          .on_request = on_request,
          .log = true,
          .max_clients = 100000,
      });
  
      // Start listening
      try listener.listen();
  
      // Start the listener
      zap.start(.{
          .threads = 4,
          .workers = 4,
      });
  }
Cookies and routes

In this example, heavily inspired from the cookies.zig example, http_params.zig example and the routes.zig example from the Zap repository, we can see how to set cookie with an eventual query parameter in one route and get the value of this same cookie in an other route. This code has some of the most basic features that a WEB API should contain.

  const std = @import("std");
  const zap = @import("zap");
  
  var routes: std.StringHashMap(zap.HttpRequestFn) = undefined;
  var allocator: std.mem.Allocator = undefined;
  
  fn setup_routes(a: std.mem.Allocator) !void {
      routes = std.StringHashMap(zap.HttpRequestFn).init(a);
      try routes.put("/setcookie", setCookie);
      try routes.put("/getcookie", getCookie);
  }
  
  fn dispatch_routes(r: zap.Request) void {
      if (r.path) |the_path| {
          if (routes.get(the_path)) |foo| {
              foo(r);
              return;
          }
      }
  
      r.sendBody(
          \\ <html>
          \\   <body>
          \\     <p><a href="/getcookie">get cookie</a></p>
          \\     <p><a href="/setcookie">set cookie</a></p>
          \\   </body>
          \\ </html>
      ) catch return;
  }
  
  fn setCookie(r: zap.Request) void {
      r.parseQuery();
      var wanted_school: []const u8 = "";
  
      if (r.getParamSlice("wanted_school")) |value| {
          wanted_school = value;
      } else {
          wanted_school = "heig";
      }
  
      r.setCookie(.{ .name = "school", .value = wanted_school }) catch return;
  
      r.sendBody("Cookie set :)") catch return;
  }
  
  fn getCookie(r: zap.Request) void {
      r.parseCookies(false);
  
      var school_name: []const u8 = "";
  
      std.debug.print("\n", .{});
      if (r.getCookieStr(allocator, "school", false)) |maybe_str| {
          if (maybe_str) |*s| {
              defer s.deinit();
              school_name = s.str;
          } else {
              school_name = "no school";
          }
      } else |_| {
          std.log.err("ERROR while reading cookie!\n", .{});
      }
  
      const ret = std.fmt.allocPrint(allocator, "Your school is {s}", .{school_name}) catch return;
      r.sendBody(ret) catch return;
  }
  
  pub fn main() !void {
      allocator = std.heap.page_allocator;
  
      // Setup up routes
      try setup_routes(allocator);
  
      // Create listener
      var listener = zap.HttpListener.init(.{
          .port = 3000,
          .on_request = dispatch_routes,
          .log = true,
          .max_clients = 100000,
      });
  
      // Start listening
      try listener.listen();
  
      // Start the listener
      zap.start(.{
          .threads = 4,
          .workers = 4,
      });
  }
Other exemples

The repo gives you a lot of other various exemples in order to get started and see what subjects interest you the most.

Benchmark folder included

The zap library provides a few other REST API frameworks to compare with Zap in a wrk folder, all the codes from the different languages/framework just return a simple "Hello World" message when their endpoint is called.

The benchmarks compare 2 different metrics:

/HEIG_ZIG/images/req_per_sec_graph.png

/HEIG_ZIG/images/xfer_per_sec_graph.png

Those benchmakrs are in my opinion well made because they assure a good thread equity between the different frameworks. By using tasket we can set processes affinities which will increase reproductility by reducing the number of context switching and by imporving cache performances. This also assures a better load balancing between the differnt tasks. TODO eviter qu 1 enleve les resources a l autre

  TSK_SRV="taskset -c 0,1,2,3"
  TSK_LOAD="taskset -c 4,5,6,7"

They also write all the outputs to /dev/null in order to avoid any IO bottleneck by trying to write to the terminal.

  $TSK_SRV zig build run /dev/null &

As well as having all the optimizations activated for all of the used frameworks and having the community optimizing and correcting them aswell, which makes it a fair comparison.

  cd wrk/rust/bythebook && cargo build --release # we can see the release flag to optimize the compiled code

I am not going to explore the results of the benchmarks further in this chapter because it will be done in the conclusion of the web chapter.

Templates

Zap also comes with a buil-int templating system that is Mustache. It is a very popular templating language that is used in a lot of other languages. We can find the documentation here.

Working with Mustache is truly easy, all is needed is writing a Mustache template file and then use it in your wanted handler on your Zap server.

Hello {{name}}, this has been processed with Mustache on my Zap server :)
  const std = @import("std");
  const zap = @import("zap");
  
  const template = @embedFile("template.tmpl");
  
  fn handler(req: zap.Request) void {
      var mustache = zap.Mustache.fromData(template) catch return;
      defer mustache.deinit();
  
      const name = "Jeremie";
  
      const tmpl = mustache.build(.{ .name = name });
      defer tmpl.deinit();
  
      if (tmpl.str()) |string_representation| {
          req.sendBody(string_representation) catch return;
      }
  
      req.sendBody("Did not render with Mustache :(") catch return;
  }
  
  pub fn main() !void {
      // Create listener
      var listener = zap.HttpListener.init(.{
          .port = 3000,
          .on_request = handler,
          .log = true,
          .max_clients = 100000,
      });
  
      // Start listening
      try listener.listen();
  
      // Start the listener
      zap.start(.{
          .threads = 4,
          .workers = 4,
      });
  }
Conclusion

Zap is a very intersting project that is not used in production as far I know by anyone except by the author of the framework itself. So I couldn't find any repository of a project uszing zap anywhere, I tried asking on the official Discord but I didn't get any answer.

Even though it should be working for almost all your use cases, it still is a microframework which means that there are not a lot of batteries included and if you need advanced features, you might have to implement those yourself.

Since Zig is a low level language and that the framework is very basic you are going to have a lot of boilerplate and small things like memory to manage manually. Those are things than can easily be avoided by using other languages like Java, Go and Node. You might end up writing a lot more code for things that could have been done easily with other solutions.

It is also important to note that this is a young project with not a lot of contributors and a very small community. So if you are going to use Zap you might have to figure out things on your own or write on the zap discord. You might aswell find codes or documentations that are oudated.

The fact that it comes with a lot of examples and that the author is very active is also a good sign and makes it easy for the user to take up the project and start coding, without it I personnaly highly doubt that I would have been able to code at least half of the examples without losing tons of hours reading the source code.

To conclude if you don't need high performances (C like), I wouldn't recommend this framework to build your REST APIs because other far easiers frameworks are available for the approximatively same performances.