Fetch

Server-side outbound HTTP, integrated with the request arena.

ctx.fetch

const resp = try ctx.fetch("https://api.example.com/items", .{});
// resp.status: u16, resp.body: []u8 (arena-owned), resp.truncated: bool

const resp2 = try ctx.fetch(url, .{
    .method = .POST,
    .body = payload,
    .content_type = "application/json",
    .extra_headers = &.{ .{ .name = "authorization", .value = bearer } },
    .max_body = 1024 * 1024,   // default 1MB; bodies beyond → truncated=true
});

The body is allocated from the request arena — it lives until the response is sent, no frees.

Typed JSON

const Item = struct { id: u32, name: []const u8 };
const items = try resp.json([]Item);

Where it runs

ctx.fetch is server-only. WASM builds return error.UnsupportedOnClient — client-side outbound work goes through a server function instead:

// island chunk: declarative — signal backed by a server fn
verve.fetchSignal(i32, "getCount", .{}, "count_bind");

// island chunk: manual round-trip
verve.registerResponseHandler("myAction", &onReply);
verve.serverFnPost("myAction", "{\"q\":\"zig\"}");

This keeps API keys on the server and turns every external call into a typed, CSRF-checked endpoint.

Composing with rendering

Fetch in the route handler, render the result — it's just control flow:

fn renderWeather(ctx: *verve.Context) !*verve.Node {
    const resp = try ctx.fetch(weather_url, .{});
    if (resp.status != 200) return errorCard(ctx);
    const w = try resp.json(Weather);
    return weatherCard(ctx, w);
}

For slow upstreams, wrap the section in a suspense boundary so the shell streams immediately.

Next: Visualization.