Skip to content

justrach/merjs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

163 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

merjs

Latest Release License Zig 0.15 Zero node_modules Experimental

merjs

Next.js-style web framework. Written in Zig. Zero Node.js.

File-based routing · SSR · Type-safe APIs · Hot reload · WASM client logic · Cloudflare Workers

Quick Start · Features · Demo · How It Works · Deploy · Changelog


The Problem

Every Node.js web framework drags in 300 MB of node_modules, a 1-3s cold start, and a JavaScript runtime you never asked for. The reason JS won the server was simple: it was already in the browser.

WebAssembly changes that. Zig compiles to wasm32-freestanding with a single flag. You can write client-side logic in Zig, compile it to .wasm, and ship it directly to the browser — no transpiler, no bundler, no runtime.

app/page.zig    →  native binary  (SSR, Zig HTTP server, < 5ms cold start)
wasm/logic.zig  →  logic.wasm     (client interactivity, runs in browser)

merjs is exploring whether you can get the full Next.js developer experience — file-based routing, SSR, type-safe APIs, hot reload — without any of its runtime weight.


Quick Start

Requirements: Zig 0.15

Option A: mer CLI (recommended)

Download the mer binary from releases, then:

mer init my-app
cd my-app
mer dev            # codegen + dev server on :3000

Option B: Clone the repo

git clone https://github.com/justrach/merjs.git
cd merjs

zig build codegen   # scan app/ and api/, generate routes
zig build wasm      # compile wasm/ → public/*.wasm
zig build serve     # dev server on :3000 with hot reload

Optional: zig build css compiles Tailwind v4 (no npm). The standalone CLI is auto-downloaded on first run, or you can install it manually via mer add css.

Visit http://localhost:3000.


Performance

Local benchmarks (Apple M-series, wrk -t4 -c50 -d10s, -Doptimize=ReleaseSmall):

merjs Next.js
Throughput 115,093 req/s ~2,060 req/s
Avg latency 0.39 ms ~77 ms
Cold start < 5 ms ~1-3 s
Binary size 260 KB N/A (interpreted)
node_modules 0 files ~300 MB / ~85k files
Build time ~3.2 s ~38 s

CI benchmarks (GitHub Actions, auto-updated on each push to main):

merjs Next.js

| Requests/sec (wrk) | 195.18 req/s | 2757.41 req/s | | Avg latency | 40.85ms 2.31ms | 72.04ms 191.42ms | | RAM usage (under load) | 4.8 MB | 71.7 MB | | Build time | 22461 ms | 30139 ms |

merjs is an early experiment — Next.js is mature and production-grade. Local and CI numbers differ due to hardware (Apple Silicon vs shared GitHub Actions VM).


Features

File-based routing — like Next.js

app/index.zig       →  /
app/dashboard.zig   →  /dashboard
app/users/[id].zig  →  /users/:id
api/users.zig       →  /api/users

Drop a .zig file, export render(), get a route. The codegen tool writes src/generated/routes.zig — a static dispatch table with zero runtime cost.

Type-safe APIs via dhi

const mer = @import("mer");

const UserModel = mer.dhi.Model("User", .{
    .name  = mer.dhi.Str(.{ .min_length = 1, .max_length = 100 }),
    .email = mer.dhi.EmailStr,
    .age   = mer.dhi.Int(i32, .{ .gt = 0, .le = 150 }),
});

pub fn render(req: mer.Request) mer.Response {
    const user = try UserModel.parse(req.body);
    return mer.typedJson(req.allocator, UserResponse{ .name = user.name });
}

Constraints are checked comptime. Validation runs at parse time. No hand-rolled JSON.

HTML builder — comptime, type-safe

const h = mer.h;

fn page() h.Node {
    return h.div(.{ .class = "container" }, .{
        h.h1(.{}, "Hello from Zig"),
        h.p(.{}, "No virtual DOM. No hydration. Just HTML."),
        h.a(.{ .href = "/about" }, "Learn more"),
    });
}

comptime { mer.lint.check(page_node); } // catches missing alts, empty titles, etc.

WASM client logic — no bundler

// wasm/counter.zig
export fn increment(n: i32) i32 { return n + 1; }
zig build wasm   # → public/counter.wasm

Load in the browser with WebAssembly.instantiateStreaming. That's it.

Hot reload — no daemon

The watcher polls app/ every 300ms, detects mtime changes, and fires an SSE event. Browser reloads. No webpack, no esbuild, no separate process.

Tailwind v4 — zero Node.js

Download the standalone Tailwind v4 CLI and place it at tools/tailwindcss. Then zig build css runs it — no npm install.


mer CLI

mer init <name>      scaffold a new project (131 KB binary, all templates embedded)
mer dev [--port N]   codegen + dev server with hot reload
mer build            production build (ReleaseSmall + prerender)
mer add <feature>    add optional features (css, wasm, worker)
mer update           update merjs dependency to latest
mer --version        print version

Download from releases — available for macOS (ARM/Intel) and Linux (x86_64/ARM64).

Or build from source:

zig build cli -Doptimize=ReleaseSmall   # → zig-out/bin/mer

Demo

Live demo: merlionjs.com — the framework's own site, built with merjs.

Singapore data dashboard: sgdata.merlionjs.com — real-time government data, SSR pages, JSON APIs, WASM, RAG-powered AI chat. Deployed on Cloudflare Workers. Zero Node.js.


Deploy to Cloudflare Workers

  1. Edit worker/wrangler.toml — set your project name, route/domain, and any R2 bindings you need.
  2. Build and deploy:
zig build worker        # compile to WASM
cd worker
wrangler deploy

If your routes use secrets (API keys, etc.), set them first: wrangler secret put MY_API_KEY

The worker/worker.js shim handles the fetch event and passes requests to the WASM binary.


How It Works

zig build codegen
  └── scans app/ + api/
  └── writes src/generated/routes.zig  (static dispatch table)

zig build serve
  └── compiles server binary
  └── binds :3000
  └── serves static files from public/ (in-memory cache)
  └── dispatches requests → hash-map route lookup (O(1) exact match)
  └── SSE watcher on app/ for hot reload

zig build worker
  └── compiles to wasm32-freestanding
  └── worker/worker.js wraps WASM in a CF Workers fetch handler

Thread model: std.Thread.Pool with CPU-count-based sizing, kernel backlog 512, 64 KB write buffers.

Layout convention: Pages returning HTML fragments are auto-wrapped by app/layout.zig. Pages returning full documents (starting with <!) bypass it.


Structure

merjs/
├── src/                    # framework runtime
│   ├── mer.zig             # public API: Request, Response, h, lint, dhi
│   ├── server.zig          # HTTP server (thread pool, hash-map router)
│   ├── ssr.zig             # SSR engine + router builder
│   ├── html.zig            # comptime HTML builder DSL
│   ├── html_lint.zig       # comptime HTML linter
│   ├── watcher.zig         # file watcher + SSE hot reload
│   ├── prerender.zig       # SSG: render pages at build time → dist/
│   └── generated/
│       └── routes.zig      # codegen output (zig build codegen) — do not edit
├── cli.zig                 # `mer` CLI entry point (init, dev, build)
├── packages/
│   └── merjs-auth/         # optional auth package
├── examples/
│   ├── desktop/            # native macOS app (experimental) — zig build desktop
│   ├── kanban/             # Kanban board demo (merboard.merlionjs.com)
│   └── singapore-data-dashboard/
├── tools/
│   ├── codegen.zig
│   └── tailwindcss         # Tailwind v4 standalone CLI (no npm)
│
│   ── merjs website (dogfooding the framework) ──
├── app/                    # website pages
├── api/                    # website API routes
├── wasm/                   # website client WASM modules
├── worker/                 # Cloudflare Workers deploy target
├── public/                 # static assets
│
├── docs/
│   └── architecture.md     # deep-dive on internals
├── .githooks/              # pre-commit (fmt+build) + pre-push (test)
└── CHANGELOG.md

See docs/architecture.md for a full breakdown of the request lifecycle, module system, streaming SSR, and desktop bridge.


Desktop (experimental)

merjs can run as a native macOS app — no Electron, no npm, one binary:

zig build desktop
open zig-out/MerApp.app

See examples/desktop/README.md for details.


Contributing

See CONTRIBUTING.md for setup instructions, build commands, and branch conventions.

Quick start:

git clone https://github.com/justrach/merjs.git
cd merjs
git config core.hooksPath .githooks   # enable pre-commit (fmt+build) and pre-push (test)
zig build test                        # run unit tests

Open an issue before submitting a large PR.


Credits

  • dhi — Pydantic-style validation for Zig
  • Tailwind CSS v4 — standalone CLI, no npm
  • kuri — E2E testing via headless Chrome
  • Zig 0.15 — the whole stack

License

MIT

About

A Zig-native web framework. File-based routing, SSR, type-safe APIs, WASM client interactivity. No Node. No npm. Just zig build serve.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages