Tokamak

Tokamak

Description

This server-side web framework has been created on April 2024 by cztomsik. It currently ammounst 238 stars on GitHub, has 4 contributors, 0 release and is currently still in early development stage. Despite that, the author is using it in two projects that are in production. One of them is open-source

One of the core concepts of Tokamak is to heavily leverage dependecy injection to make the development of web applications easier. As far as I know it is the only web framework in Zig that does that.

If you have trouble understanding what Tokamak is but you are more familiar with the Java world, know that the author claims that Tokamak is to http.zig what Spring it to Servlets in the Java world. Basically, it is an abstraction layer on top of basics utilises like handling requests by adding things like Dependency Injection.

Dependency Injection (DI)

To illustrate what DI is and why it is uses in Tokamak, let's have a look at an example given to me by the author itself.

When writing a WEB application you might need multiple objects (dependencies) in various handlers. Those dependencies often being databases, loggers, etc. The most intuitive way to do it would be to pass each needed dependency as a parameter to each handler.

  // Our dependencies
  var logger = newLogger();
  var db = newDatabase();

  // Our handlers
  web.get("/hello", hello(req, logger))
  web.get("/register", register(req, db, logger))
  web.get("/complex", complex(req, rep, db, logger, ...)) // This can get out of hand quickly

This solution might look cool at first, but one of the main reason I or anyone did not write it in Zig is because it is simply impossible to express that in the language because Zig does not support closures. Except if you implement it yourself, like this blog explains but that is not the point.

An other basic and straight-forward solution would be to use global variables and just use them in the handlers. It works great but it is not a good practice to have global variables in your code. This makes the code coupled and harder to test and maintain.

  // Our dependencies
  var logger = newLogger();
  var db = newDatabase();

  // Our handlers
  web.get("/hello", hello)
  web.get("/register", register)
  web.get("/complex", complex)

  // Simply use the global variables in the handlers
  ...

An other solution would be to use a one big shared struct to store the dependencies, but this is not a good practice aswell as it also makes the code very coupled and dependant on that context variable.

  var context = {
    logger: newLogger(),
    db: newDatabase(),
    ...
  }

  // Our dependencies
  var logger = newLogger();
  var db = newDatabase();

  // Our handlers
  web.get("/hello", hello(req, context))
  web.get("/register", register(req, context))
  web.get("/complex", complex(req, context))

This previous solution is the one used in this http.zig example where the Global struct act as the context.

The solution Tokamak puts forward is to use DI. How it is done under the hood is that all the handlers have the same signature fn (*tk.Context) anyerror!void which allows the framework to chain the handlers. The framework is going to do a bit a magic for you by injecting all the depencies you need in the handlers when you need them by using this *tk.Context object. So from the developer point of view, you just have to write the handlers and the framework will take care of the rest. This also makes the code easier to test and more maintainable since everything is decoupled now, you could easily copy paste one of your handlers in an other project and it should work just fine.

  // Our dependencies
  var logger = newLogger();
  var db = newDatabase();

  // Our handlers
  web.get("/hello", hello)
  web.get("/register", register)
  web.get("/complex", complex)

  // Simply add the paramters you need to your handler function signature
  ...
  fn register(myDb) {
    // Do something with myDb
  }
Simple example

A basic Tokamak application is really easy to get up and running really fast.

  const tk = @import("tokamak");
  
  pub fn main() !void {
      const allocator = std.heap.page_allocator;
      _ = try tk.Server.run(allocator, handler, .{ .port = 8080 });
  }
  
  fn handler() ![]const u8 {
      return "Wow tokamak is so easy!";
  }

A lot of the work relies on dependency injection to make the work easier. Like here with the *tk.Request and the std.mem.Alloctor injectors.

  // Those 2 parameters are injected by the framework
  fn handler(req: *tk.Request, allocator: std.mem.Allocator) ![]const u8 {
    for (req.query_params) |param| {
        std.debug.print("{s} = {s}\n", .{ param.name, param.value });
    }

    const username = req.getQueryParam("username");
    return std.fmt.allocPrint(allocator, "Hello {s} !", .{username.?}); // Note that here we assume that the user passed a "username" query parameter
  }

We can also inject our own dependecies like a database connection or a logger.

  var db: ?*c.sqlite3 = undefined;
  const rc = c.sqlite3_open("test.db", &db);
  _ = try tk.Server.run(allocator, handler, .{ .port = 8080, .injector = try tk.Injector.from(.{db.?}) });

And then use it in any of our handlers by just adding it as a parameter.

  fn populate(db: *c.sqlite3) ![]const u8 {
      const req =
          \\CREATE TABLE IF NOT EXISTS EMPLOYEES(
          \\ID INT PRIMARY KEY     NOT NULL, 
          \\NAME           TEXT    NOT NULL,
          \\AGE            INT     NOT NULL,
          \\ADDRESS        CHAR(50),
          \\SALARY         REAL )
      ;
  
      const rc = c.sqlite3_exec(db, req, null, null, null); // Not working properly + no error handling, just for the sake of the example
      return "Database populated!";
  }

The framework also supports things like Routing and Middlewares, which are very similar to those of other framework like Express.js.

A typical handler for a Tokamak application might look like this.

  const handler = tk.chain(.{
      tk.logger(.{}),
      tk.get("/", tk.send("Coucou")),
      tk.get("contact", contact),
      tk.get("populate", populate),
      tk.get("html", tk.sendStatic("src/static/index.html")), // Serve a static file
      tk.group("/api", tk.router(api)), // Allows for a group of routes to be prefixed with /api
      tk.send(error.NotFound), // Fallback if no route is found
  });
Cookies

It is also easy to set cookies.

  fn cookie(rep: *tk.Response) ![]const u8 {
      try rep.setCookie("name", "chocolate", .{});
      return "The cookie has been set!";
  }
Future of Tokamak

All the examples above are compatible only with 0.11.0. Since at the time of writing this 0.13.0 is already out and the author is working on a new breaking version of the framework, I will not dig any deeper for the moment. But I plan to remake this documentation when the new version is out and 0.13.0 compliant.

Conclusion

From an user experience point of view everything is very understandable and easy to grasp compared to a micro-framework like Zap. The documentation is minimal but still explores all of the main features of what it has to offer. The project is very young and small and mainly made by a single person. The author has been very responsive and useful to answer a lot of my questions so I thank him very much for that.

Performance wise, the framework is not the fastest out there. The main reason is because it uses std.http under the hood which is the blocking and slow official library. The author claims to change that to use http.zig in the next update which should make the framework a lot faster.

The main branch has not been updated for a long time now and is only compatible to old Zig versions, so I personnaly do not recommend downgrading your Zig version to work with this Tokamak versino because soon a new breaking releases with lot of changes will be out, by then I will update this documentation.

To summarize, this framework is great but for the moment I recommend waiting for the next release which is going to come with updated documentations as well.