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 replies

Calling 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.