Middleware#
A zyn app declares its global middleware in src/app.zig:
pub const middleware: zyn.MiddlewareStack = &.{
.use(zyn.MethodOverride, .{ .param = "_method" }),
.use(zyn.RequestLogger, .{
.filtered_params = &.{
"passw",
"email",
"secret",
"token",
"_key",
"crypt",
"salt",
"otp",
"ssn",
"cvv",
"cvc",
},
.ignored_params = &.{"_method"},
}),
.use(zyn.Cors, .{
.origins = .{ .allow = &.{"https://app.example.com"} },
.methods = &.{ "GET", "HEAD", "POST" },
.headers = &.{"Content-Type"},
.max_age_seconds = 600,
}),
};
zyn.MiddlewareStack is the ordered declaration collection. A middleware module defines a compile-time Config type and a handle(config, middleware) function whose second parameter is *zyn.Middleware. Call try middleware.next() to continue to later middleware and route handling; return without calling it to short-circuit after preparing a response.
Custom middleware#
A middleware module supplies its compile-time configuration and one request handler:
const zyn = @import("zyn");
pub const Config = struct {
header_value: []const u8,
};
pub fn handle(
comptime config: Config,
middleware: *zyn.Middleware,
) !void {
if (middleware.headers.get("x-block-request") != null) {
return middleware.respond(try .text(
"request blocked",
.{ .status = .forbidden },
));
}
try middleware.headers.set("x-example", config.header_value);
if ((try middleware.cookies.encrypted.get("session")) != null) {
try middleware.headers.set("x-session", "present");
}
try middleware.next();
}
Register the module with .use(Implementation, config). middleware.request and middleware.response are live *zyn.http.Request and *zyn.http.Response values. middleware.headers, middleware.cookies, middleware.db, and middleware.logger provide the same request-scoped capabilities available to other roles. Application-defined locals are available through middleware.request.locals.
middleware.headers is the same zyn.Headers capability used by actions and hooks. get(name) reads an inbound request header; set(name, value) sets or replaces an outbound response header, and remove(name) removes an outbound response header. Names match case-insensitively. Writes do not change what get reads.
Call next() exactly once to continue. Request, response, cookie, and local changes remain visible after downstream success or failure. A second call returns error.ContinuationAlreadyCalled. Returning without next() short-circuits after the middleware prepares the response; respond(outcome) applies an explicit zyn.Response and transfers its ownership to the framework. respond() does not return from the handler or disable next(). Use return middleware.respond(...) to apply a response and exit the handler when short-circuiting. After next(), respond() can replace a response that has not yet been committed to the client; it cannot replace bytes already sent.
Code after try middleware.next() runs only on downstream success. Use defer for non-fallible cleanup or logging that must run on both success and failure:
pub fn handle(
comptime _: Config,
middleware: *zyn.Middleware,
) !void {
defer middleware.logger.info("middleware finished", .{});
try middleware.next();
}
Use catch when handling a downstream error explicitly; return the error to propagate it or prepare a response to recover. Request and response state is refreshed before next() returns on either path. A downstream response may already have been committed, so post-next() work cannot assume that headers or body bytes can still be changed.
middleware.reroute(method) changes the effective method and reselects the route, guards, hooks, layouts, parameters, and final HEAD behavior. Call it before next(); calling it afterward returns error.ContinuationAlreadyCalled. Rerouting as OTHER preserves the request's custom method_string for route selection and downstream handling; rerouting as a known method clears it.
zyn.MethodOverride routes URL-encoded POST forms as PATCH, PUT, or DELETE when the configured form parameter contains that method. Declare it first so downstream middleware and routing observe the effective method.
<form method="post" action="/items/42">
<input type="hidden" name="_method" value="PATCH">
<button type="submit">Update item</button>
</form>
zyn.RequestLogger records requests to stdout and logs/dev.log:
0123456789abcdef0123456789abcdef 200 GET /users/42 · routes.show
total measures downstream middleware and request handling. view measures the action excluding its database time, and db sums database calls made by all request roles, including middleware registered before the logger. The payload contains request-body fields. Route parameters and query fields are not included.
Configure filtered_params to replace sensitive matching values with "[FILTERED]" and ignored_params to omit matching fields. Both lists use case-sensitive byte substrings, apply recursively to nested JSON, and default to empty. Ignore takes precedence when both policies match. Generated projects use the sensitive-name list shown above and omit the method-override field.
zyn.Cors grants configured browser origins access to cross-origin responses and answers valid preflight OPTIONS requests with an empty 204. Exact origin allowlists are recommended. Methods and non-safelisted request headers use explicit allowlists; exposed response headers, credentials, and preflight cache duration are optional. Denied ordinary requests continue without CORS permission headers, while denied preflights return 403 Forbidden.
Fetch Metadata CSRF protection is configured through route defaults rather than global middleware. See the routing guide for .csrf(.protected) and .csrf(.exempt).