Merjs
A Zig-native web framework. File-based routing, SSR, type-safe APIs, WASM client interactivity. No Node. No npm. Just zig build serve.
Install / Use
/learn @justrach/MerjsREADME
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 csscompiles Tailwind v4 (no npm). The standalone CLI is auto-downloaded on first run, or you can install it manually viamer 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 | | ---------------------- | -------------------------- | ------------------------------ |
<!-- BENCH:START -->| Requests/sec (wrk) | 195.14 req/s | 2375.92 req/s | | Avg latency | 40.85ms 2.36ms | 76.01ms 189.44ms | | RAM usage (under load) | 4.7 MB | 72.1 MB | | Build time | 27506 ms | 34143 ms |
<!-- BENCH:END -->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
- Edit
worker/wrangler.toml— set your project name, route/domain, and any R2 bindings you need. - 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+bu
