Experimental - under active development
A TypeScript runtime
built for speed
Built from scratch in Zig as a lightweight alternative to Node.js, Deno, and Bun. One binary, zero dependencies, instant cold starts.
$ zig build -Doptimize=ReleaseFast
$ ./zig-out/bin/zigttp-server -e "function handler(r) { return Response.json({hello:'world'}) }"
listening on http://127.0.0.1:8080
$ curl http://localhost:8080/
{"hello":"world"}
What makes it different
Where Node.js, Deno, and Bun optimize for generality, zigttp optimizes for a single use case: running a TypeScript request handler as fast as possible, then getting out of the way.
Opinionated subset
TypeScript with the footguns removed. No classes, no this,
no var, no while loops. Unsupported features fail
at parse time with a suggested alternative, not at runtime with a cryptic stack trace.
JSX as a first-class primitive
The parser handles JSX and TSX directly. No Babel, no build step, no transpilation. Write server-rendered HTML with components and get back a string.
Compile-time evaluation
comptime() folds expressions at load time, modeled after Zig's comptime.
Hash a version string, uppercase a constant, precompute a config value -
all before the handler runs.
Native modules over polyfills
Common FaaS needs - JWT auth, JSON Schema validation, caching, crypto, SQL -
are implemented in Zig and exposed as zigttp:* virtual modules
with zero interpretation overhead.
OpenAPI from type inference
-Dopenapi generates an OpenAPI 3.0 spec directly from handler
type analysis. Response schemas, path parameters, and effect metadata -
no annotations needed.
Handler effect classification
Each virtual module call carries a proven effect annotation. The compiler
aggregates them into handler properties: read_only,
retry_safe, deterministic - all in the contract.
Clean, predictable code
Functions in, responses out. No middleware chains, no decorator magic, no hidden state.
function handler(request) {
if (request.url === "/api/health") {
return Response.json({
status: "ok",
runtime: "zts",
timestamp: Date.now(),
});
}
return Response.json(
{ error: "Not Found" },
{ status: 404 }
);
}
function HomePage() {
return (
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
<p>Welcome to zigttp!</p>
</body>
</html>
);
}
function handler(request) {
if (request.url === "/") {
return Response.html(renderToString(<HomePage />));
}
return Response.text("Not Found", { status: 404 });
}
import { routerMatch } from "zigttp:router";
const routes = {
"GET /": getHome,
"GET /health": getHealth,
"GET /users/:id": getUser,
"POST /echo": postEcho,
};
function handler(req) {
const match = routerMatch(routes, req);
if (match) {
req.params = match.params;
return match.handler(req);
}
return Response.json(
{ error: "Not Found" },
{ status: 404 }
);
}
import { parseBearer, jwtVerify } from "zigttp:auth";
import { schemaCompile, validateJson } from "zigttp:validate";
schemaCompile("user", JSON.stringify({
type: "object",
required: ["name", "email"],
properties: {
name: { type: "string", minLength: 1 },
email: { type: "string", minLength: 5 },
}
}));
function handler(req) {
const token = parseBearer(req.headers["authorization"]);
if (!token) return Response.json(
{ error: "unauthorized" }, { status: 401 }
);
const auth = jwtVerify(token, "my-secret");
if (!auth.ok) return Response.json(
{ error: auth.error }, { status: 401 }
);
if (req.method === "POST") {
const result = validateJson("user", req.body);
if (!result.ok) return Response.json(
{ errors: result.errors }, { status: 400 }
);
return Response.json({ user: result.value }, { status: 201 });
}
return Response.json({ user: auth.value });
}
function TodoForm() {
return (
<form
hx-post="/todos"
hx-target="#todo-list"
hx-swap="beforeend">
<input type="text" name="text" required />
<button type="submit">Add Todo</button>
</form>
);
}
function handler(request) {
if (request.url === "/" && request.method === "GET") {
return Response.html(
renderToString(<TodoForm />)
);
}
if (request.url === "/todos" && request.method === "POST") {
const html = renderToString(
<div class="todo-item">New todo</div>
);
return Response.html(html);
}
return Response.text("Not Found", { status: 404 });
}
Performance
Powered by zts - a pure Zig JavaScript engine with a JIT compiler, NaN-boxed values, and hidden classes. It skips everything a FaaS handler doesn't need.
JS Engine Performance (vs QuickJS)
Cold Start Comparison: zigttp vs Node.js vs Deno vs Bun
Runtime initialization is only 3ms. macOS dynamic linker accounts for 80-90ms. Linux static builds target 18-33ms total cold start.
Two-layer architecture
zigttp is a server. zts is the engine. Both are written in Zig. Together they form a purpose-built runtime for handling HTTP requests with JavaScript.
zigttp server
- HTTP/1.1 listener and parser
- HandlerPool with lock-free slot acquisition
- Static file serving with LRU cache
- CLI with inline eval, CORS, memory limits
- Build-time handler precompilation
- Native outbound HTTP bridge
zts engine
- Two-pass compiler (parse to IR, then bytecode)
- Stack-based VM with baseline JIT (x86-64, ARM64)
- NaN-boxed values with hidden classes
- Polymorphic inline cache (8-entry PIC)
- Generational GC with hybrid arena allocation
- Native TypeScript/TSX stripping
Virtual modules
Import native Zig implementations with zero JS interpretation overhead. Common FaaS functionality, none of the npm baggage.
zigttp:auth
JWT signing and verification (HS256), bearer token parsing, webhook signature verification, timing-safe comparison.
parseBearer, jwtVerify, jwtSign, verifyWebhookSignature
zigttp:validate
JSON Schema validation with compile-once-validate-many pattern. Supports type, required, properties, min/max, enum.
schemaCompile, validateJson, validateObject, coerceJson
zigttp:cache
In-memory key-value store with namespace isolation, TTL expiry, LRU eviction. Persists across requests in the same pool slot.
cacheGet, cacheSet, cacheDelete, cacheIncr, cacheStats
zigttp:crypto
SHA-256 hashing, HMAC-SHA256, base64 encoding and decoding. All implemented in Zig with no OpenSSL dependency.
sha256, hmacSha256, base64Encode, base64Decode
zigttp:router
Pattern-matching HTTP router with path parameter extraction. Declare routes as an object, match with a single call.
routerMatch
zigttp:env
Environment variable access for configuration. Simple key-value interface for deployment secrets and config.
env
zigttp:sql
SQLite query execution with build-time schema validation via -Dsql-schema. Named parameters, contract-tracked queries.
sql, sqlOne, sqlMany, sqlExec
zigttp:compose
Guard-based handler composition with the pipe operator (|>). Desugared to flat sequential checks at compile time.
guard
zigttp:io
Structured concurrent I/O without async/await. Overlaps multiple fetchSync calls on OS threads (max 8 concurrent).
parallel, race
zigttp:durable
Write-ahead oplog for crash recovery. Idempotent run/step API with timer and signal-based workflow suspension.
run, step, sleep, sleepUntil, waitSignal
zigttp vs Node.js vs Deno vs Bun
How does zigttp compare to the major TypeScript and JavaScript runtimes? Each makes different trade-offs. zigttp sacrifices generality for verification and speed.
| zigttp | Node.js | Deno | Bun | |
|---|---|---|---|---|
| Written in | Zig | C++ | Rust | Zig + C++ |
| JS engine | zts (custom) | V8 | V8 | JavaScriptCore |
| TypeScript | Native stripping | Via transpiler | Native stripping | Native stripping |
| Cold start | ~71ms | ~200-300ms | ~150-200ms | ~100-130ms |
| Binary size | 1.2MB | ~80MB | ~130MB | ~90MB |
| Memory baseline | 4MB | ~30MB | ~25MB | ~20MB |
| Compile-time verification | Yes | No | No | No |
| Auto sandboxing | Yes | No | Permissions | No |
| Deterministic replay | Yes | No | No | No |
| Dependencies | Zero | npm | npm / jsr | npm |
| Use case | Serverless / FaaS | General purpose | General purpose | General purpose |
Get started
Requires Zig 0.16.0 or later.
1 Build
$ git clone https://github.com/srdjan/zigttp
$ cd zigttp
$ zig build -Doptimize=ReleaseFast
2 Run
# Inline handler
$ ./zig-out/bin/zigttp-server -e "function handler(r) { return Response.json({ok:true}) }"
# Or with a file
$ ./zig-out/bin/zigttp-server examples/handler.jsx
3 Deploy
# Production build with embedded handler
$ zig build -Doptimize=ReleaseFast -Dhandler=handler.js
# Container: scratch base, 1.2MB total
$ docker build -t myapp .
The language subset
zts implements ES5 with select ES6+ extensions. Every unsupported feature is detected at parse time with a helpful error message suggesting an alternative.
Supported
- Strict mode, let/const
- Arrow functions
- Template literals
- Destructuring, spread
- for...of (arrays)
- Optional chaining, nullish coalescing
- Pipe operator (
|>) - Array methods (map, filter, reduce, find)
- Object.keys/values/entries
- Typed arrays
- JSX/TSX (native parsing)
- TypeScript type stripping
- comptime() evaluation
- range(start, end, step)
Intentionally excluded
class- use functions and objectsthis/new- use closuresvar- use let/constwhile- use for...of with range()try/catch- use Result typesasync/await- sequential by designPromises- one request at a time- Regular expressions
- Event loop
require/ dynamic import
Frequently asked questions
What is zigttp?
zigttp is a JavaScript runtime built from scratch in Zig, designed specifically for serverless and FaaS workloads. It compiles your handler into a single binary with zero dependencies, achieving 3ms cold starts, a 1.2MB binary size, and 80K requests per second throughput.
How is zigttp different from Node.js, Deno, and Bun?
Node.js, Deno, and Bun are general-purpose JavaScript runtimes that support the full language specification. zigttp takes the opposite approach: it implements an opinionated TypeScript subset that removes footgun features (classes, this, async/await, exceptions) at parse time. This constraint enables compile-time verification of handler correctness, automatic least-privilege sandboxing, and deterministic replay - none of which are possible when the runtime must support arbitrary JavaScript. If you need a general-purpose runtime, use Node.js, Deno, or Bun. If you want provably correct serverless handlers with minimal cold starts, zigttp is the better fit.
What JavaScript features does zigttp support?
zigttp supports arrow functions, const/let, destructuring, template literals, for...of, optional chaining, nullish coalescing, the pipe operator, array methods (map, filter, reduce), JSX/TSX, TypeScript type stripping, and comptime() evaluation. Features like classes, var, while loops, async/await, and try/catch are intentionally excluded with parse-time errors that suggest alternatives.
How fast is zigttp compared to Node.js, Deno, and Bun?
zigttp cold starts at ~71ms (embedded bytecode) vs ~100-130ms for Bun, ~150-200ms for Deno, and ~200-300ms for Node.js. The binary is 1.2MB vs 80-130MB for the others. Memory baseline is 4MB vs 20-30MB. Runtime initialization alone is 3ms. These gains come from Zig's zero-overhead abstractions and a custom JS engine (zts) purpose-built for short-lived request handlers.
Is zigttp free and open source?
Yes. zigttp is MIT licensed and available on GitHub. The entire runtime, compiler, and standard library of virtual modules are open source.
What are virtual modules?
Virtual modules (zigttp:auth, zigttp:cache, zigttp:sql, zigttp:io, etc.) are built-in capabilities implemented in Zig with zero interpretation overhead. They provide JWT authentication, key-value caching, SQLite queries, parallel I/O, durable execution, and more - all accessible through standard import syntax and tracked by the compiler for automatic sandboxing.
Can I use zigttp instead of Bun for serverless functions?
Yes, if your use case is request-response serverless handlers. Bun is a general-purpose runtime that supports the full Node.js API, npm packages, and arbitrary JavaScript. zigttp is purpose-built for FaaS: it trades generality for a 1.2MB binary, 4MB memory footprint, compile-time handler verification, and automatic sandboxing. For serverless workloads where cold start time and resource efficiency matter, zigttp is a strong Bun alternative.
Why is zigttp built in Zig instead of Rust or C++?
Zig provides manual memory control with comptime metaprogramming, no hidden allocations, and no runtime overhead - ideal for building a JavaScript engine where every microsecond of cold start matters. Unlike C++, Zig has no hidden constructors or exceptions. Unlike Rust, Zig avoids borrow checker complexity while still preventing undefined behavior through explicit error handling. Zig's comptime evaluation also powers zigttp's build-time handler verification.
Does zigttp support TypeScript?
Yes. zigttp natively strips TypeScript type annotations at parse time with zero overhead - no transpilation step, no tsc, no build tool. You write TypeScript (or TSX for JSX components) and zigttp handles it directly. The supported subset includes const/let type annotations, function parameter types, return types, interfaces, and generics for type checking.
Can I migrate from Node.js or Express to zigttp?
zigttp is not a drop-in replacement for Node.js. It implements a restricted TypeScript subset and does not support npm packages, the Node.js API, or middleware frameworks like Express. However, if your serverless handlers are stateless request-response functions, the migration path is straightforward: rewrite handlers using zigttp's functional style and virtual modules. The result is a provably correct, single-binary deployment with dramatically lower cold starts.