Skip to content

Middlewares

While Tokamak doesn't have Express-style middleware, it achieves the same functionality through nested routes and context handlers.

Basic Middleware

Create middleware by wrapping routes:

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

            return ctx.next();
        }
    };

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

const routes: []const tk.Route = &.{
    logger(&.{
        .get("/", hello),
        .get("/users", getUsers),
    }),
};

Context Handlers

Middleware handlers receive *tk.Context and must call ctx.next():

zig
fn timing(ctx: *tk.Context) anyerror!void {
    const start = std.time.milliTimestamp();

    try ctx.next();

    const duration = std.time.milliTimestamp() - start;
    log.info("Request took {}ms", .{duration});
}

Request-Scoped Dependencies

Since Zig doesn't have closures, use ctx.nextScoped() to pass data to downstream handlers:

zig
fn auth(ctx: *tk.Context) anyerror!void {
    const db = ctx.injector.get(*Database);
    const token = ctx.req.getHeader("Authorization") orelse
        return error.Unauthorized;

    const user = try jwt.parse(token);

    // Make user available to downstream handlers
    return ctx.nextScoped(&.{ user });
}

// Downstream handler can now inject User
fn getProfile(user: User) !Profile {
    return Profile{ .id = user.id, .name = user.name };
}

TIP

Middleware handlers must use ctx.injector.get(T) to access dependencies manually, as they don't support automatic DI syntax.

Common Middleware Patterns

Authentication

zig
fn requireAuth(children: []const tk.Route) tk.Route {
    const H = struct {
        fn handle(ctx: *tk.Context) anyerror!void {
            const token = ctx.req.getHeader("Authorization") orelse
                return error.Unauthorized;

            const user = try validateToken(token);
            return ctx.nextScoped(&.{ user });
        }
    };

    return .{ .handler = &H.handle, .children = children };
}

const routes: []const tk.Route = &.{
    .get("/public", publicHandler),
    requireAuth(&.{
        .get("/private", privateHandler),
    }),
};

CORS

zig
fn cors(ctx: *tk.Context) anyerror!void {
    try ctx.res.setHeader("Access-Control-Allow-Origin", "*");
    try ctx.res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");

    if (ctx.req.method == .OPTIONS) {
        try ctx.res.send("", .{});
        return;
    }

    try ctx.next();
}

Error Handling

zig
fn errorHandler(ctx: *tk.Context) anyerror!void {
    ctx.next() catch |err| {
        log.err("Request failed: {}", .{err});

        const status: u16 = switch (err) {
            error.Unauthorized => 401,
            error.NotFound => 404,
            error.ValidationError => 400,
            else => 500,
        };

        try ctx.res.status(status).json(.{
            .error = @errorName(err)
        }, .{});
    };
}

Composing Middleware

Stack multiple middleware together:

zig
const routes: []const tk.Route = &.{
    errorHandler(
        logger(
            cors(
                requireAuth(&.{
                    .get("/api/users", getUsers),
                })
            )
        )
    ),
};

Or use groups for better organization:

zig
const routes: []const tk.Route = &.{
    .group("/", &.{
        .handler(errorHandler),
        .handler(logger),
        .handler(cors),

        .get("/public", publicHandler),

        .group("/api", &.{
            .handler(requireAuth),
            .get("/users", getUsers),
            .post("/posts", createPost),
        }),
    }),
};