v0.1.0-dev.651

Ziex

/

Build fast & type-safe full-stack web apps with Zig.

JSX like syntax or just like HTML with having access to Zig's control flow.

$ curl -fsSL https://ziex.dev/install | bash
> powershell -c "irm ziex.dev/install.ps1 | iex"

Features

It's Fast

~100x faster SSR than Next.js. Compiles to native code instead of running JavaScript.

Compile-Time Safety

Zig's type system catches bugs at compile time. No runtime surprises, no GC.

Familiar Syntax

Familiar JSX like syntax or just like HTML with having access to Zig's control flow.

Server-Side Rendering

Server-side rendering by default. Pages render on the server with zero configuration.

Static Site Generation

Static site generation by default. Pages render on the server with zero configuration.

File System Routing

Folder structure defines routes. No configs, no magic strings, just files in folders.

Client-side Rendering

Optional client-side rendering for interactive experiences when you need it.

Control Flow in Zig's Syntax

if/else, for/while, switch, standard control flow works as expected. It's just Zig.

Developer Tooling

CLI, hot reload, and editor extensions for the best DX.

Performance Benchmarks

Performance

View benchmark code & methodology →
Each framework runs in a Docker container limited to 2 CPU cores and 2 GB RAM.

Requests per second (higher is better)
Ziex
23k
Requests/sec23,877
P50 Latency0.91 ms
P99 Latency37.99 ms
15k
Requests/sec15,762
P50 Latency2.54 ms
P99 Latency17.99 ms
3.8k
Requests/sec3,824
P50 Latency10.15 ms
P99 Latency47.17 ms
644
Requests/sec644
P50 Latency75.01 ms
P99 Latency141.99 ms
Dockerized SSR endpoint hit with 5k requests at 50 concurrent connections, averaged over 2 runs.
Peak memory under load
Ziex
12.4 MB
Idle Memory12.4 MB
Peak Memory12.4 MB
19.9 MB
Idle Memory19.9 MB
Peak Memory19.9 MB
133.4 MB
Idle Memory133.4 MB
Peak Memory133.4 MB
233.6 MB
Idle Memory98.9 MB
Peak Memory233.6 MB
Peak container RSS during load test. Lower is better. Hover for idle memory.

Feature Examples

Short, focused snippets that show how each feature feels in practice.

Control Flow
const ControlIfProps = struct { is_admin: bool };
pub fn ControlIf(
    allocator: zx.Allocator,
    props: ControlIfProps,
) zx.Component {
    return (
        <div @{allocator}>
            {if (props.is_admin) (
                <span>Admin</span>
            ) else (
                <span>Member</span>
            )}
        </div>
    );
}
pub fn ControlIfOptional(allocator: zx.Allocator) zx.Component {
    const maybe_name: ?[]const u8 = "Zig";
    return (
        <div @{allocator}>
            {if (maybe_name) |name| (<span>{name}</span>)}
        </div>
    );
}
pub fn ControlIfError(allocator: zx.Allocator) zx.Component {
    const parsed =
        std.fmt.parseInt(u32, "42", 10);

    return (
        <div @{allocator}>
            {if (parsed) |value| (
                <span>{value}</span>
            ) else |err| (
                <span>{@errorName(err)}</span>
            )}
        </div>
    );
}
pub fn ControlWhile(allocator: zx.Allocator) zx.Component {
    var i: usize = 0;
    return (
        <ul @{allocator}>
            {while (i < 3) : (i += 1) (<li>Item {i + 1}</li>)}
        </ul>
    );
}
pub fn ControlWhileOptional(allocator: zx.Allocator) zx.Component {
    var maybe: ?u32 = 1;
    return (
        <ul @{allocator}>
            {while (maybe) |value| : (maybe = if (value < 3) value + 1 else null) (
                <li>{value}</li>
            )}
        </ul>
    );
}
pub fn ControlWhileError(allocator: zx.Allocator) zx.Component {
    var first: bool = true;
    return (
        <div @{allocator}>
            {while (parseOnce(&first)) |value| (
                <span>{value}</span>
            ) else |err| (
                <span>{@errorName(err)}</span>
            )}
        </div>
    );
}

fn parseOnce(first: *bool) !u32 {
    if (first.*) {
        first.* = false;
        return 42;
    }
    return error.Done;
}
Caching
pub fn CacheComponent(allocator: zx.Allocator) zx.Component {
    return (<Expensive @{allocator} @caching="10s" />);
}

fn Expensive(allocator: zx.Allocator) zx.Component {
    // std.Thread.sleep(std.time.ns_per_s * 3);
    return (<div @{allocator}></div>);
}
pub fn Page(
    ctx: zx.PageContext,
) zx.Component {
    // std.Thread.sleep(std.time.ns_per_s * 3);
    return (
        <div @allocator={ctx.arena}>
            <h1>Expensive Page</h1>
            <p>I take too long to load.</p>
        </div>
    );
}

pub const options = zx.PageOptions{
    .caching = .{ .seconds = 300 },
};
File System Routing
pub fn Page(
    ctx: zx.PageContext,
) zx.Component {
    return (
        <div @allocator={ctx.arena}>
            <h1>Home</h1>
        </div>
    );
}
pub fn Layout(
    ctx: zx.LayoutContext,
    children: zx.Component,
) zx.Component {
    return (
        <html @allocator={ctx.arena}>
            <body>
                {children}
            </body>
        </html>
    );
}
Components
const ButtonProps = struct { label: []const u8 };
pub fn Button(
    ctx: *zx.ComponentCtx(ButtonProps),
) zx.Component {
    return (
        <button @allocator={ctx.allocator}>
            {ctx.props.label}
        </button>
    );
}
pub fn Fragment(
    allocator: zx.Allocator,
) zx.Component {
    return (
        <fragment @{allocator}>
            <span>One</span>
            <>
                <p>Two</p><p>Three</p>
            </>
        </fragment>
    );
}
pub fn SpreadProps(
    allocator: zx.Allocator,
) zx.Component {
    const attrs =
        .{ .class = "btn", .id = "cta" };
    const class = "primary";
    return (
        <section @{allocator}>
            <button {..attrs}>Spreading</button>
            <button {class}>Shorthand</button>
        </section>
    );
}
pub fn DynamicAttr(
    allocator: zx.Allocator,
) zx.Component {
    const is_active = true;
    const class = if (is_active) "active" else "idle";
    const color = if (is_active) "green" else "red";
    return (
        <section @{allocator}>
            <button class={class}>Go</button>
            <button style=`color: {color};`>
                Template String
            </button>
        </section>
    );
}
Dynamic Path
pages/[id]/page.zx
pub fn Page(ctx: zx.PageContext) zx.Component {
    const id = ctx.request.getParam("id");
    return (
        <section @allocator={ctx.arena}>
            <h1>User {id}</h1>
        </section>
    );
}
API Route
route.zig
pub fn PUT(ctx: zx.RouteContext) !void {
    try ctx.response.json(.{ .status = "ok" });
}

pub fn POST(ctx: zx.RouteContext) !void {
    const Data = struct { id: u32 };
    const data =
        try ctx.request.json(Data, .{});

    try ctx.response.json(.{
        .id = data.?.id,
        .created = true,
    });
}
WebSocket
routes/ws/route.zig
pub fn GET(ctx: zx.RouteContext) !void {
    try ctx.socket.upgrade({});
}

pub fn Socket(ctx: zx.SocketContext) !void {
    try ctx.socket.write(
        try ctx.fmt("{s}", .{ctx.message}),
    );
}

pub fn SocketOpen(ctx: zx.SocketOpenContext) !void {
    try ctx.socket.write("Opened!");
}

Familiar Syntax

Familiar JSX like syntax or just like HTML with having access to Zig's control flow.

example.zx
pub fn QuickExample(allocator: zx.Allocator) zx.Component {
    const is_loading = true;
    const chars = "Hello, ZX Dev!";
    var i: usize = 0;
    return (
        <main @allocator={allocator}>
            <section>
                {if (is_loading) (<h1>Loading...</h1>) else (<h1>Loaded</h1>)}
            </section>

            <section>
                {for (chars) |char| (<span>{char}</span>)}
            </section>

            <section>
                {for (users) |user| (
                    <Profile name={user.name} age={user.age} role={user.role} />
                )}
            </section>

            <section>
                {while (i < 10) : (i += 1) (<p>{i}</p>)}
            </section>
        </main>
    );
}

fn Profile(allocator: zx.Allocator, user: User) zx.Component {
    return (
        <div @allocator={allocator}>
            <h1>{user.name}</h1>
            <p>{user.age}</p>
            {switch (user.role) {
                .admin => (<p>Admin</p>),
                .member => (<p>Member</p>),
            }}
        </div>
    );
}

const UserRole = enum { admin, member };
const User = struct { name: []const u8, age: u32, role: UserRole };

const users = [_]User{
    .{ .name = "John", .age = 20, .role = .admin },
    .{ .name = "Jane", .age = 21, .role = .member },
};

const zx = @import("zx");