Server functions
Declare a struct of functions; get typed HTTP endpoints. The server walks app.Actions at comptime and generates a /api/<fn> route for every public function — JSON decode, dispatch, JSON encode, CSRF checks included.
// src/app/api.zig
pub const Actions = struct {
pub fn getCount(_: struct {}) !i32 {
return last_count.load(.monotonic);
}
pub fn incrementCount(_: struct {}) i32 {
return last_count.fetchAdd(1, .monotonic) + 1;
}
pub fn addTodo(args: struct { text: []const u8 }) !void {
const trimmed = std.mem.trim(u8, args.text, &std.ascii.whitespace);
if (trimmed.len == 0) return error.EmptyTodo;
...
}
};Rules of the shape:
- exactly one argument, a struct (possibly empty
struct {}); - return any JSON-encodable value,
void, or an error union of either; - Actions may run on multiple worker threads — guard shared state (atomics, mutexes, fixed pools). There is no allocator handed in; return slices must be static or threadlocal.
Calling from the client (WASM)
Build-time codegen produces a typed stub per action in the app_client module:
const app_client = @import("app_client");
// fire-and-forget POST
app_client.incrementCount_post(.{});
// correlated round-trip: reply lands in your handler
app_client.getCount_call(.{}, onCount);Lower-level, from any island chunk:
verve.serverFnPost("logMessage", "{\"text\":\"hi\"}");
verve.registerResponseHandler("getCount", myHandler); // route repliesCalling during SSR
Already on the server? Skip the HTTP round-trip entirely:
const count = ctx.serverFn(api.Actions.getCount, .{});Progressive enhancement: actionForm
Forms post natively when JavaScript is off and dispatch through WASM when it's on — same endpoint either way:
ctx.actionForm(.{ .post = "/api/incrementCount" }).children(.{
ctx.button("+").type_("submit").onClick("increment_counter"),
})actionForm pre-wires a hidden CSRF field. The native path replies 303-redirect back to the Referer; the enhanced path intercepts the click and routes through the chunk export named in onClick.
Form field decoding
Native form posts decode application/x-www-form-urlencoded fields into the args struct by name — <input name="text"> feeds args.text. Hidden inputs carry indices and ids:
ctx.actionForm(.{ .post = "/api/removeTodo" }).children(.{
ctx.input().type_("hidden").name("index").attrFmt("value", "{d}", .{i}),
ctx.button("×").type_("submit"),
})Security
Every generated endpoint checks the request Origin against Host and verifies the __csrf form field against the __verve_csrf cookie on native posts. See Security.
Next: Head management.