1
0
Fork
You've already forked tokamak
0
Web framework for Zig that leverages dependency injection for clean, modular application development.
  • Zig 100%
2025年11月17日 09:44:56 +01:00
.github/workflows Add VitePress documentation with GitHub Pages deployment 2025年10月21日 15:21:36 +00:00
docs container: rename add() -> provide(), addOverride() -> override(), etc. 2025年11月17日 09:44:56 +01:00
examples ssr: WIP components (few small steps, still a lot of work ahead) 2025年11月16日 21:08:13 +01:00
src container: rename add() -> provide(), addOverride() -> override(), etc. 2025年11月17日 09:44:56 +01:00
.gitignore Add VitePress documentation with GitHub Pages deployment 2025年10月21日 15:21:36 +00:00
build.zig zig15: quickfix tk.setup() #2 2025年10月05日 10:58:26 +02:00
build.zig.zon update httpz 2025年10月02日 16:24:59 +02:00
LICENSE server: use inplace adapter, return by value from Server.init() 2025年03月04日 19:31:32 +01:00
package-lock.json Add VitePress documentation with GitHub Pages deployment 2025年10月21日 15:21:36 +00:00
package.json Add VitePress documentation with GitHub Pages deployment 2025年10月21日 15:21:36 +00:00
README.md container: rename add() -> provide(), addOverride() -> override(), etc. 2025年11月17日 09:44:56 +01:00

Tokamak

📚 Documentation

Tokamak is a server-side framework for Zig, built around http.zig and a simple dependency injection container.

Note that it 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, sanitization, etc.

Recent changes

  • renamed few bundle.addXxx() methods to bundle.provide(), bundle.override(), ...
  • renamed inj.call0(fun)inj.call(fun), inj.call(fun, ...args)inj.callArgs(fun, ...args)
  • opt dependencies were removed, ie. you can no longer inject ?Cfg - it was undocumented, incomplete, subtly broken, and not worth the extra complexity
  • multi-mod API has changed considerably
  • there's a new cli module
  • injecting tk.Injector is deprecated, use *tk.Injector
  • multi-module support (cross-module initializers, providers, overrides)
  • Switched to http.zig for improved performance over std.http.
  • Implemented hierarchical and introspectable routes.
  • Added basic Swagger support.
  • Added tk.static.dir() for serving entire directories.

Installation

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

Then in your build.zig:

consttokamak=@import("tokamak");pubfnbuild(b:*std.Build)void{...constexe=b.addExecutable(.{...});...// Add tokamaktokamak.setup(exe,.{});}

Getting Started

Simple things should be easy to do.

conststd=@import("std");consttk=@import("tokamak");constroutes:[]consttk.Route=&.{.get("/",hello),};fnhello()![]constu8{return"Hello";}pubfnmain()!void{vargpa=std.heap.GeneralPurposeAllocator(.{}){};constallocator=gpa.allocator();defer_=gpa.deinit();varserver=trytk.Server.init(allocator,routes,.{.listen=.{.port=8080}});tryserver.start();}

Dependency Injection

The framework is built around the concept of dependency injection. This means that your handler function can take any number of parameters, and the framework will try to provide them for you.

Notable types you can inject are:

  • std.mem.Allocator (request-scoped arena allocator)
  • *tk.Request (current request, including headers, body reader, etc.)
  • *tk.Response (current response, with methods to send data, set headers, etc.)
  • *tk.Injector (the injector itself, see below)
  • and everything you provide yourself

For example, you can easily write a handler function which will create a string on the fly and return it to the client without any tight coupling to the server or the request/response types.

fnhello(arena:std.mem.Allocator)![]constu8{returnstd.fmt.allocPrint(arena,"Hello {}",.{std.time.timestamp()});}

If you return any other type than []const u8, the framework will try to serialize it to JSON.

fnhello()!HelloRes{return.{.message="Hello"};}

If you need a more fine-grained control over the response, you can inject a *tk.Response and use its methods directly.

But this will of course make your code tightly coupled to respective types and it should be avoided if possible.

fnhello(res:*tk.Response)!void{tryres.json(.{.message="Hello"},.{});}

Custom Dependencies

You can also provide your own (global) dependencies by passing your own *tk.Injector to the server.

pubfnmain()!void{vardb=trysqlite.open("my.db");varinj=tk.Injector.init(&.{.ref(&db)},null)varserver=trytk.Server.init(allocator,routes,.{.injector=&inj,.port=8080});tryserver.start();}

For advanced dependency injection features like multi-module support, intrusive interfaces, and lifecycle hooks, see the Advanced Dependency Injection section below.

Middleware

While Tokamak doesn't have Express-style middleware, it achieves the same functionality through nested routes. Since routes can be nested and the prefix, path, and method fields are optional, you can create powerful middleware patterns.

Here's how to create a simple logging middleware:

fnlogger(children:[]constRoute)tk.Route{constH=struct{fnhandleLogger(ctx:*Context)anyerror!void{log.debug("{s} {s}",.{@tagName(ctx.req.method),ctx.req.url});returnctx.next();}};return.{.handler=&H.handleLogger,.children=children};}constroutes=[]consttk.Route=&.{logger(&.{.get("/",hello),}),};

Middleware handlers receive a *Context and return anyerror!void. They can perform pre-processing, logging, authentication, etc., and then call ctx.next() to continue to the next handler in the chain.

Request-Scoped Dependencies

Since Zig doesn't have closures, you can't capture variables from the outer scope. Instead, Tokamak allows you to add request-scoped dependencies that will be available to downstream handlers:

fnauth(ctx:*Context)anyerror!void{constdb=ctx.injector.get(*Db);consttoken=tryjwt.parse(ctx.req.getHeader("Authorization"));constuser=db.find(User,token.id)catchnull;returnctx.nextScoped(&.{user});}

Note: Middleware handlers need to use ctx.injector.get(T) to access dependencies manually, as they don't support the automatic dependency injection syntax.

Routing

Tokamak includes an Express-inspired router that supports path parameters and wildcards. It can handle up to 16 path parameters and uses the * character for wildcards.

consttk=@import("tokamak");constroutes:[]consttk.Route=&.{.get("/",hello),// fn(...deps).get("/hello/:name",helloName),// fn(...deps, name).get("/hello/:name/:age",helloNameAge),// fn(...deps, name, age).get("/hello/*",helloWildcard),// fn(...deps).post("/hello",helloPost),// fn(...deps, body).post0("/hello",helloPost0),// fn(...deps)...};

For more organized routing, use the Route.router(T) method with a DSL-like struct:

constroutes:[]consttk.Route=&.{tk.logger(.{}),.get("/",tk.send("Hello")),// Classic Express-style routing.group("/api",&.{.router(api)}),// Structured routing with a module.send(error.NotFound),};constapi=struct{pubfn@"GET /"()[]constu8{return"Hello";}pubfn@"GET /:name"(arena:std.mem.Allocator,name:[]constu8)![]constu8{returnstd.fmt.allocPrint(arena,"Hello {s}",.{name});}};

Error Handling

Tokamak handles errors gracefully by automatically serializing them to JSON:

fnhello()!void{// This will send a 500 response with {"error": "TODO"}returnerror.TODO;}

Static Files

Serve static files easily with built-in helpers:

constroutes:[]consttk.Route=&.{.get("/",tk.static.file("public/index.html")),};

Serve entire directories:

constroutes:[]consttk.Route=&.{tk.static.dir("public",.{}),};

Use with wildcard routes for more flexibility:

constroutes:[]consttk.Route=&.{tk.get("/assets/*",tk.static.dir("assets",.{.index=null})),};

If you want to embed some files into the binary, you can specify such paths to the tokamak.setup() call in your build.zig file.

consttokamak=@import("tokamak");...tokamak.setup(exe,.{.embed=&.{"public/index.html",},});

In this case, only the files listed in the embed array will be embedded into the binary and any other files will be served from the filesystem.

MIME types

The framework will try to guess the MIME type based on the file extension, but you can also provide your own in the root module.

pubconstmime_types=tk.mime_types++.{.{".foo","text/foo"},};

Configuration

For a simple configuration, you can use the tk.config.read(T, opts) function, which will read the configuration from a JSON file. The opts parameter is optional and can be used to specify the path to the config file and parsing options.

constCfg=struct{foo:u32,bar:[]constu8,};constcfg=trytk.config.read(Cfg,.{.path="config.json"});

There's also experimental tk.config.write(T, opts) function, which will write the configuration back to the file.

Process Monitoring

The tk.monitor(procs) function runs multiple processes in parallel and automatically restarts them if they crash. This creates a self-healing application that stays running even after unexpected failures.

monitor(.{.{"server",&runServer,.{8080}},.{"worker",&runWorker,.{}},...});

It takes a tuple of { name, fn_ptr, args_tuple } triples as input.

Note: This feature requires a system with fork() support. It takes over the main thread and forks processes, which may lead to unexpected behavior if used incorrectly. Use with caution.

Advanced Dependency Injection

Multi-Module System

Tokamak supports a powerful multi-module system where dependencies are automatically resolved across module boundaries. Modules are Zig structs where fields become dependencies:

constSharedModule=struct{db_pool:DbPool,pubfnconfigure(bundle:*tk.Bundle)void{// Add more deps conditionally, override how they should be initialized, add hooks... (see below)}};constWebModule=struct{server:tk.Server,routes:[]consttk.Route=&.{...},};// Register modules when creating a Containerpubfnmain()!void{trytk.app.run(Server.start,&.{SharedModule,WebModule,});}

The Bundle API provides compile-time dependency configuration:

  • addModule(M) - Add all fields of module M as dependencies
  • provide(T, how) - Provide a single dependency with initialization strategy
  • override(T, how) - Override dependency initialization (works across modules)
  • mock(T, how) - Test-only override for mocking
  • expose(T, field) - Expose a reference to a struct field as dependency
  • addInitHook(fn) - Add runtime initialization callback
  • addDeinitHook(fn) - Add runtime cleanup callback

Initialization strategies:

  • .auto - Automatic initialization (uses T.init() if available, otherwise autowires struct fields)
  • .init - Explicitly use T.init() method
  • .autowire - Initialize struct by injecting all fields
  • .factory(fn) - Use custom factory function
  • .initializer(fn) - Use initializer function (receives pointer to initialize)
  • .value(v) - Use provided comptime value directly

Intrusive Interface Pattern

Tokamak supports an intrusive interface pattern for pluggable implementations. Types with an interface field are automatically registered for dependency injection, ie:

const AppModule = struct {
 http_client: StdClient, // Define concrete implementation
};
// client will point to &StdClient.interface
fn handler(client: *HttpClient) !void {
 ...
}

Testing

For testing, you can override dependencies:

constTestModule=struct{pubfnconfigure(bundle:*Bundle)void{bundle.mock(Database,.value(MockDatabase{}));bundle.mock(EmailService,.factory(createMockEmailService));}};// Run test with mocked dependenciesconstct=tryContainer.init(test_allocator,&.{AppModule,TestModule});ct.injector.call(myTestFun)

License

MIT