Dependency Injection
Tokamak is built around dependency injection, allowing handlers to declare their dependencies as function parameters.
Basic Injection
Handlers can request dependencies simply by adding parameters:
fn hello(arena: std.mem.Allocator) ![]const u8 {
return std.fmt.allocPrint(arena, "Hello {}", .{std.time.timestamp()});
}Built-in Dependencies
These types can be injected into any handler:
std.mem.Allocator- Request-scoped arena allocator*tk.Request- Current HTTP request*tk.Response- Current HTTP response*tk.Injector- The DI container itself
Response Types
Handlers have flexible return types:
Return String
fn hello() ![]const u8 {
return "Hello";
}Return JSON
Any type other than []const u8 is automatically serialized to JSON:
const HelloRes = struct { message: []const u8 };
fn hello() !HelloRes {
return .{ .message = "Hello" };
}Manual Response Control
For fine-grained control, inject *tk.Response:
fn hello(res: *tk.Response) !void {
try res.json(.{ .message = "Hello" }, .{});
}TIP
Avoid tight coupling to *tk.Response when possible. Prefer returning values directly.
Custom Dependencies
Provide your own global dependencies via a custom injector:
pub fn main() !void {
var db = try sqlite.open("my.db");
var cache = try Cache.init();
var inj = tk.Injector.init(&.{
.ref(&db),
.ref(&cache),
}, null);
var server = try tk.Server.init(allocator, routes, .{
.injector = &inj,
});
try server.start();
}Now handlers can inject these dependencies:
fn getUser(db: *sqlite.Database, cache: *Cache, id: []const u8) !User {
if (cache.get(id)) |user| return user;
const user = try db.find(User, id);
try cache.set(id, user);
return user;
}Multi-Module System
For larger applications, organize dependencies into modules:
const SharedModule = struct {
db_pool: DbPool,
cache: Cache,
};
const WebModule = struct {
server: tk.Server,
routes: []const tk.Route = &.{ /* ... */ },
};
pub fn main() !void {
try tk.app.run(tk.Server.start, &.{
SharedModule,
WebModule,
});
}Bundle API
Configure module dependencies with the Bundle API:
const AppModule = struct {
db: Database,
pub fn configure(bundle: *tk.Bundle) void {
// Provide dependencies
bundle.provide(Logger, .factory(createLogger));
// Override existing dependencies
bundle.override(Cache, .factory(createRedisCache));
// Add lifecycle hooks
bundle.addInitHook(onInit);
bundle.addDeinitHook(onDeinit);
}
};Initialization strategies:
.auto- Automatic initialization.init- UseT.init()method.autowire- Inject all struct fields.factory(fn)- Custom factory function.value(v)- Compile-time value
Testing with Mocks
Override dependencies for testing:
const TestModule = struct {
pub fn configure(bundle: *tk.Bundle) void {
bundle.mock(Database, .value(MockDatabase{}));
bundle.mock(EmailService, .factory(createMockEmail));
}
};
test "user registration" {
const ct = try tk.Container.init(
test_allocator,
&.{ AppModule, TestModule }
);
defer ct.deinit();
try ct.injector.call(testUserRegistration);
}Intrusive Interfaces
Types with an interface field are automatically registered:
const HttpClient = struct {
get: *const fn(*HttpClient, []const u8) anyerror![]const u8,
};
const StdClient = struct {
interface: HttpClient,
// ... implementation
};
const AppModule = struct {
http_client: StdClient,
};
// Handlers receive the interface pointer
fn fetchData(client: *HttpClient, url: []const u8) ![]const u8 {
return client.get(client, url);
}