# Tokamak: a web framework for Zig built around an injector

> Tokamak is a web framework for Zig that puts a dependency injection container at the centre, so a handler is just a function whose parameters describe what it needs. It ships with a router, nested-route middleware, request-scoped values and Swagger support, and it expects to sit behind a reverse proxy rather than to be one.

**cztomsik/tokamak** — Web framework for Zig that leverages dependency injection for clean, modular application development.

- Repository: https://github.com/cztomsik/tokamak
- Website: https://tomsik.cz/tokamak/
- Stars: 641 · Forks: 25
- Language: Zig
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/cztomsik-tokamak

## A framework whose centre is a container, not a router

Tokamak is described as a web application framework for Zig, built around http.zig and a simple dependency injection container, and that ordering is the point. In most web frameworks the router is the centre and everything else is decoration, and the shape of a handler is fixed by the framework. Here the container is the centre, and the router is a consumer of it. The README states the consequence plainly: your handler function can take any number of parameters, and the framework will try to provide them for you. That is the whole idea in one sentence. The dependency injection container is described as simple, and the framework's ambitions are correspondingly scoped, with the main pieces being a server, a router, an injector and the type helpers around them. The project describes itself as aiming at clean, modular application development, and the mechanism is meant to make module boundaries a matter of declaring what a component needs rather than of wiring it up by hand. The audience is a Zig developer who wants a service with structure, and the project is young enough that the shape of the API is still settling, which is the first thing to weigh before depending on it.

## Fetching the package and hooking it into build.zig

Tokamak is a Zig package, installed with the Zig package manager, and the README gives the command.

```bash
zig fetch --save "git+https://github.com/cztomsik/tokamak#main"
```

The dependency then has to be wired into the build, and the README's snippet shows that this is a single call in the build function.

```zig
const tokamak = @import("tokamak");

pub fn build(b: *std.Build) void {
    ...
    const exe = b.addExecutable(.{ ... });
    ...

    // Add tokamak
    tokamak.setup(exe, .{});
}
```

The setup call takes the executable and an options struct, which is the extension point for whatever configuration the framework needs. The repository layout is consistent with a Zig project of this kind, with a build.zig and a build.zig.zon at the root, the library under src, a docs directory, an examples directory and a package.json for the documentation site. The examples are the fastest way to see the range of what the framework covers, and the directory names cover a plain hello service, a hello app, a hello CLI, a blog, a to-do application backed by SQLite through an ORM, and a webview application. The CLI one matters more than it sounds, because the recent changes list mentions a new cli module, so command line handling is part of the framework rather than something every project has to assemble. One requirement is stated up front and is not negotiable for the main branch: it requires Zig 0.17.0 or newer.

## Handlers whose parameters are the dependency list

The dependency injection mechanism is best shown by the shape of a handler. The simplest one takes no arguments and returns a byte slice, which the framework sends as the response body.

```zig
fn hello() ![]const u8 {
    return "Hello";
}
```

Add a parameter and the framework supplies it. The notable injectable types are listed in the README: std.mem.Allocator, which is a request scoped arena allocator, the current request as a pointer to a tk.Request with headers and a body reader, the current response as a pointer to a tk.Response with methods to send data and set headers, the injector itself, and anything you have provided yourself.

```zig
fn hello(arena: std.mem.Allocator) ![]const u8 {
    return std.fmt.allocPrint(arena, "Hello {}", .{std.time.timestamp()});
}
```

The return type decides the encoding. If you return []const u8 it is sent as is, and if you return any other type the framework will try to serialize it to JSON, which is why a small struct response needs no extra ceremony.

```zig
fn hello() !HelloRes {
    return .{ .message = "Hello" };
}
```

There is a third option with a cost, and the README flags it. Injecting a pointer to a tk.Response gives you fine grained control, because you can call res.json directly, but the README says plainly that this makes your code tightly coupled to the framework types and that it should be avoided if possible. The arena allocator is the detail worth internalising, since a request scoped arena means the handler can allocate freely and the framework frees it when the request ends, with no explicit cleanup in user code.

## Middleware by nesting routes instead of chaining

Tokamak does not have Express-style middleware, and the README explains what it has instead: the same functionality achieved through nested routes, where the prefix, path and method fields are optional, so the nesting pattern can express middleware chains. The example is a logging middleware written as a function that takes the child routes and returns a route, with the handler defined in a local struct and the child chain invoked through ctx.next().

```zig
fn logger(children: []const tk.Route) tk.Route {
    const H = struct {
        fn handleLogger(ctx: *tk.Context) anyerror!void {
            std.log.debug("{s} {s}", .{ @tagName(ctx.req.method), ctx.req.url.path });

            return ctx.next();
        }
    };

    return .{ .handler = &H.handleLogger, .children = children };
}
```

A middleware handler receives a pointer to a tk.Context and returns an error union of void, and it can do pre-processing, logging or authentication before calling ctx.next() to continue down the chain. The distinction from a conventional middleware stack is structural rather than cosmetic. Express composes a flat array of functions that each receive the request and a next callback, while here a middleware is a route with children, so the call chain is part of the routing tree and the same nesting that groups routes also groups behaviour. The README also documents the router style variant, where a group is created with a group call, which is the same idea expressed through a different syntax.

## Request-scoped values, and why Zig forces this design

One feature has a reason that is specific to Zig, and the README states it directly: since Zig does not have closures, you cannot capture variables from the outer scope. That single language limitation shapes how a request handler passes state to the work below it. Instead of capturing, you add request-scoped dependencies that will be available to downstream handlers, and the mechanism is a call to nextScoped with the values you want to inject.

```zig
fn auth(ctx: *tk.Context) anyerror!void {
    const db = try ctx.injector.get(*Db);
    const token = try jwt.parse(ctx.req.header("authorization") orelse "");
    const user = db.find(User, token.id) catch null;

    return ctx.nextScoped(&.{ user });
}
```

The example is an authentication middleware that looks up the database and the token, and hands the resulting user to the rest of the chain. The one constraint the README attaches is that middleware handlers have to use ctx.injector.get with the type to reach dependencies manually, because they do not support the automatic dependency injection syntax that ordinary handlers have. That asymmetry is worth knowing, because it means a middleware is written in a different style from the handlers it wraps. The other piece of the design is the global side, custom dependencies provided through the multi-module system, where you define a struct with the dependencies as fields and the framework resolves them automatically.

```zig
const App = struct {
    db: Db,
    server: tk.Server,
    routes: []const tk.Route = &.{ ... },
};

pub fn main(init: std.process.Init) !void {
    try tk.app.run(init, tk.Server.start, &.{App});
}
```

## Hierarchical routes, the router DSL and Swagger output

The router is described as Express-inspired, with path parameters and wildcards, and it can handle up to 16 path parameters while using the star character for wildcards. The route table shows the variants, and the comments in it are a compact description of the calling convention: a handler receives its dependencies first and then the path parameters, while a post variant receives the request body.

```zig
const routes: []const tk.Route = &.{
    .get("/", hello),                        // fn(...deps)
    .get("/hello/:name", helloName),         // fn(...deps, name)
    .get("/hello/*", helloWildcard),         // fn(...deps)
    .post("/hello", helloPost),              // fn(...deps, body)
    .post0("/hello", helloPost0),            // fn(...deps)
    ...
};
```

For larger applications there is a second style, using the Route.router method with a DSL-like struct, where functions are named with the method and path in a single identifier, which is what makes a struct-per-module layout possible.

```zig
const api = struct {
    pub fn @"GET /"() []const u8 {
        return "Hello";
    }

    pub fn @"GET /:name"(arena: std.mem.Allocator, name: []const u8) ![]const u8 {
        return std.fmt.allocPr
```

Alongside the routing work, the recent changes list records that routes are now hierarchical and introspectable, that basic Swagger support was added, and that a static directory serving helper was added. Introspectable routes matter because they are what a framework needs to generate documentation or to answer a discovery request, and the Swagger support is the first consumer of that. The first API in this style is also the clearest statement of the design: the struct functions are the routes, the router method turns the struct into a route tree, and a group call mounts that tree under a path prefix.

## Churn, the Zig version, and the reverse proxy posture

The last thing to weigh is stability, because the recent changes list is long and specific. The bundle methods that added dependencies were renamed to bundle.provide and bundle.override, the injector call methods were renamed so that inj.call0 became inj.call and the variant taking arguments became inj.callArgs, and injecting a tk.Injector by value is deprecated in favour of a pointer. Optional dependencies were removed outright, and the README gives the reasons: the feature was undocumented, incomplete, subtly broken, and not worth the extra complexity. The multi-module API changed considerably, with a pull request linked for the details, and cross-module initializers, providers and overrides are now supported. The HTTP layer was also swapped, from the standard library's std.http to http.zig, which the README justifies as improved performance. A framework that renames its own methods from one iteration of the main branch to the next and changes its multi-module API is a moving target, and a project that has to survive a version bump should read that list before it touches the code. The other standing constraint is the deployment model. The README says plainly that the framework is not designed to be used alone, but with a reverse proxy in front of it, like Nginx or Cloudfront, which will handle SSL, caching and sanitization. That is a sane division of labour for a Zig service, and it also means the framework is not the thing standing between you and the public internet. The licence is MIT, the documentation is at tomsik.cz/tokamak, and the repository has no GitHub releases, so there is no tagged version to pin and the main branch is the thing you fetch.

## Conclusion

Adopt Tokamak if you are building a Zig service and want handlers that declare their dependencies as function parameters, with a router, a JSON path and Swagger support already wired. It does not fit a team that needs a framework with a stable API, because the README lists renamed methods, a changed multi-module API and a recent switch of the underlying HTTP library on the main branch, and it does not fit anyone planning to serve TLS directly, since the project states it is not designed to be used alone but behind a reverse proxy. Before you build on it, pin the Zig version at 0.17.0 or newer as the README requires, and read the recent changes list, because several of those renames will break code written against earlier examples.

## FAQ

### What is Tokamak and what does dependency injection look like in it?

Tokamak is a web application framework for Zig built around http.zig and a simple dependency injection container. A handler is a function whose parameters are its dependencies, and the framework provides them, including a request scoped arena allocator, the current request, the current response, the injector, and anything you have registered yourself.

### How do I add Tokamak to a Zig project?

Run zig fetch --save with the git+https://github.com/cztomsik/tokamak#main URL, then import it in build.zig with @import and call tokamak.setup(exe, .{}) inside the build function. The main branch requires Zig 0.17.0 or newer.

### Does Tokamak support Express-style middleware?

Not in the Express form. The README says it achieves the same functionality through nested routes, where the prefix, path and method fields are optional, so a middleware is a route with children whose handler calls ctx.next() to continue the chain.

### How do I pass data to the next handler in Tokamak?

Zig has no closures, so request scoped dependencies are added with ctx.nextScoped, passing the values to inject into the downstream handlers. Middleware handlers themselves reach dependencies through ctx.injector.get with the type, since they do not support the automatic injection syntax.

### What happens to the response when a handler returns something other than a byte slice?

The framework will try to serialize it to JSON, so a struct response is returned without extra ceremony. If you need finer control you can inject a pointer to a tk.Response and use its methods, although the README notes that this couples your code to the framework types and should be avoided if possible.

## Sources

- [cztomsik/tokamak on GitHub](https://github.com/cztomsik/tokamak)
- [Issues](https://github.com/cztomsik/tokamak/issues)
- [License: MIT](https://github.com/cztomsik/tokamak/blob/main/LICENSE)
- [Project website](https://tomsik.cz/tokamak/)
- [README](https://github.com/cztomsik/tokamak/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cztomsik-tokamak
