Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

justrach/merjs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

196 Commits

Repository files navigation

merjs

Latest Release License Zig 0.16 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.16

Option A: One-line install (recommended)

curl -fsSL https://merjs.trilok.ai/install.sh | bash

Then:

mer init my-app
cd my-app
mer dev # dev server on :3000 with hot reload

Option B: mer CLI from releases

Install the latest mer binary from releases:

curl -fsSL https://raw.githubusercontent.com/justrach/merjs/main/scripts/install-mer.sh | sh

Or download manually, then:

mer init my-app
cd my-app
mer dev

Option C: 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.09 req/s | 2448.68 req/s | | Avg latency | 40.86ms 2.19ms | 76.24ms 192.32ms | | RAM usage (under load) | 6.4 MB | 71.5 MB | | Build time | 23350 ms | 37854 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

Quick install from source checkout:

zig build cli -Doptimize=ReleaseSmall
install -m 755 zig-out/bin/mer /usr/local/bin/mer

If you use merjs as a Zig dependency, prefer its exported API instead of reaching into package paths directly:

const merjs_dep = b.dependency("merjs", .{});
const mer_mod = merjs_dep.module("mer");
const server_mod = merjs_dep.module("server");

Fresh mer init apps also vendor their own tools/codegen.zig, so route generation no longer depends on internal merjs package paths.


Troubleshooting

Server crashes or "Connection refused"

Problem: Server stops when terminal closes or shows "ERR_CONNECTION_REFUSED"

Solutions:

1. Run in foreground (development):

mer dev
# or
zig build serve

Server runs in terminal. Press Ctrl+C to stop.

2. Run in background with nohup (keeps running):

# Build first
zig build -Doptimize=ReleaseFast
# Run with nohup (won't stop when terminal closes)
nohup ./zig-out/bin/merjs --port 3000 --no-dev > merjs.log 2>&1 &
# Check it's running
curl http://localhost:3000
# View logs
tail -f merjs.log
# Stop server
pkill -f "merjs"

3. Common fixes:

# Port already in use?
lsof -i :3000
kill -9 <PID>
# Or use different port
./zig-out/bin/merjs --port 3001 --no-dev
# Check binary exists
ls -la zig-out/bin/merjs
# Clean build
rm -rf .zig-cache zig-out
zig build -Doptimize=ReleaseFast

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

AltStyle によって変換されたページ (->オリジナル) /