TECHNOLOGY LAB · SYSTEMS ENGINEERING

Baz / Get started

Build your first App

Get started

A working App in one command.

Use exact Zig 0.16.0 and Python 3. Clone Baz, then start the demo. The pinned HTTP engine is fetched automatically.

01 · Start the App
git clone https://github.com/technologylab-ai/baz.git
cd baz
zig build run-app -Doptimize=ReleaseSafe -- --port 8080
02 · In another terminal, from the Baz directory
curl 'http://127.0.0.1:8080/hello?name=Hello%20Zig'
curl --data 'value=x+y%2Bz' http://127.0.0.1:8080/form
curl -F 'files[]=@.zig-version' http://127.0.0.1:8080/upload

The current executable defaults to IPv4 loopback over plain HTTP/1.1. Stop it with Ctrl-C. On Windows, use curl.exe; executables have an .exe suffix. For embedding Baz in your own project, follow the package guide and independent consumer fixture.

An ordinary Zig App

A struct. A method.
A clear place for everything.

Define your shared state, choose App(Shared), and register an endpoint. Each App owns its lifecycle and uses one router. The context gives you the request, your state, route captures, and a bounded response draft.

const Hello = struct {
    pub fn get(_: *Hello, ctx: *Context) !void {
        const query = try ctx.request.query();
        if (query.firstRaw("name")) |param| {
            var decoded: [1024]u8 = undefined;
            const name = try web.params.percentDecodeInto(param.value_raw, &decoded);
            return ctx.response.text(200, name); // copies before stack storage ends
        }
        return ctx.response.text(200, ctx.shared.greeting);
    }
};

This excerpt uses Context = web.App(Shared).Context. Register it with try app.endpoint("/hello", &hello). See the complete App example for initialization, stable state, and shutdown.

Start with explicit state.

Shared state and endpoint instances stay at stable addresses through App destruction. Register routes before startup. Synchronize mutable state used by concurrent callbacks.

Return a finished response.

Text, bytes, copied headers, JSON, and standard formatting all use bounded output. A failed handler discards its private draft before error mapping.

OWNERSHIPThis name is temporary, so response.text copies it. For retained input and immutable startup assets, borrowBody borrows the payload independently of staging capacity, within the total response limit. See the copy paths and limits → See a complete file-response handler →

Send the first bytes. Keep working.

Write a little. Flush it to the client. Pause, then send the next update—all inside the same handler.

fn progress(ctx: *Application.Context) !void {
    try ctx.response.header("Cache-Control", "no-cache");
    var stream = try ctx.response.stream(200, "text/plain; charset=utf-8", .{});
    const out = stream.writer();
    try out.writeAll("Starting…\n");
    try out.flush();
    try ctx.sleep(.fromMilliseconds(500));
    try out.print("Completed step {d} of {d}\n", .{ 1, 2 });
    try out.flush();
    try ctx.sleep(.fromMilliseconds(500));
    try out.writeAll("Done.\n");
    try stream.finish();
}

STREAMINGout is a standard std.Io.Writer. Each flush() sends the current bytes before the handler continues.

Updates as they happen.

Progress reports, generated text, and incremental output can reach clients before the handler returns. Use curl -N to watch the example arrive in three parts.

Bounded while you wait.

A stream uses one existing application worker and fixed output storage. A slow reader applies backpressure; cancellation releases the waiting handler safely. The HTTP I/O loop keeps running.

The complete example selects fixed workers. Headers are final after the first flush, and response size and request time remain bounded. Read the streaming guide → Native streaming evidence →

Many waiting streams. A small worker pool.

For higher concurrency, typed continuations return flush, wait, or finish. Waiting releases the executor while bounded typed state stays alive. Middleware runs once; completion, timeout, and disconnect clean up that state. Works inline or with fixed workers. Read the continuation guide →

Current limit: stream writes copy bytes. You cannot insert a borrowBody() call between flushes. See the response combination rules →

Send a file. Skip the body copy.

Already have a large image or file in memory? borrowBody() lets Baz send those existing bytes directly, avoiding a copy into its response buffer. A 5 MiB file stays in its original storage instead of being copied into another 5 MiB framework buffer.

This complete handler shows the API with an embedded HTML file: GET / returns the file’s contents as the entire response body. Use the same call with your image bytes and content type. Small bodies below the default 256-byte threshold—including this tiny HTML file—may still be copied.

const Shared = struct {};
const Application = web.App(Shared);

fn index(ctx: *Application.Context) !void {
    return ctx.response.borrowBody(200, "text/html; charset=utf-8", @embedFile("assets/serve_index.html"));
}

Here web = @import("baz"). The complete example registers try app.route("GET", "/", index). @embedFile includes the file at compile time, so this handler performs no filesystem I/O. Run it with zig build run-serve -Doptimize=ReleaseSafe -- --port 8080.

FEWER COPIESresponse.bytes() copies your body into bounded staging. borrowBody() avoids that payload copy for large retained bodies; a large file can use a small output arena. Headers and normal kernel transfers still have copying costs. See exactly where copies happen →

borrowBody() prepares one complete body. When this handler returns successfully, App publishes status 200, HTTP headers, and the file bytes. There is no body prefix or suffix. The embedded bytes stay alive for transmission.

Headers and handler work still fit.

Authenticate, choose an asset, and add response headers in your handler. Headers can be added before or after borrowBody() while the response is unpublished. A large immutable file can use a small output arena; the total response limit still applies.

Streaming + borrowing: not yet.

Writing and flushing a prefix, calling borrowBody() with an image, then continuing the stream is currently unsupported. Borrowing the whole body and starting a stream are separate choices—even before the first flush. Use the stream writer’s writeAll(image) to put image bytes between other streamed chunks; that path copies through bounded staging.

Verified on native Linux, macOS, and Windows: the separate 5 MiB fixture sends the original payload with a 4 KiB arena and no framework payload copy. Read the copy-counter and ownership evidence → Read what can be combined →

From a background job to a live page.

The complete jobs example puts Mustache, cookie sessions and SSE together. A startup producer publishes progress; bounded notifications wake waiting streams while their workers remain available. Event IDs support explicit, finite replay, and each resumed callback checks the session again. Run the application → See the job studio → Read the SSE and notification API →