The whole web framework. One 170KB binary.
Routes, JSON, WebSockets, templates, uploads, sessions and production guardrails — compiled to a single static binary with zero dependencies. No node_modules. No runtime to install on the server. This is the framework powering nyxlang.com.
$ curl -sSf https://nyxlang.com/install.sh | sh
import "std/web" import "nyx-serve/src/server" fn hello(req: Request) -> Response { return response_json(200, "{\"msg\":\"hello\"}") } fn user(req: Request) -> Response { let id: String = req.params.get("id") return response_json(200, "{\"user\":\"" + id + "\"}") } fn main() { let app: App = app_new() app_get(app, "/", hello) app_get(app, "/users/{id}", user) serve_app(app, 3000, 8) }
Handlers are plain functions
A route maps a pattern to a function. Path params with
{id}, query strings, cookies and the parsed body
arrive already split on the Request — you read
fields, you return a Response. Nothing hides
behind decorators or magic globals.
- Path params —
req.params.get("id") - JSON body in —
req_json(req)gives you a Map - JSON out —
response_json_map(201, data)
fn create_note(req: Request) -> Response { let body: Map = req_json(req) let title: String = body.get("title") var out: Map = map_new() out.insert("created", title) return response_json_map(201, out) }
fn chat_handler(info: Array) -> int { let fd: int = info[0] let headers: Array = info[2] let key: String = http_find_header(headers, "Sec-WebSocket-Key") tcp_write(fd, ws_handshake_response(key)) let frame: String = tcp_read_partial(fd, 4096) tcp_write(fd, ws_frame("echo: " + ws_parse(frame))) tcp_close(fd) return 1 // we own the socket now } // in main(): app_ws("/ws/chat/{room}", chat_handler)
WebSockets are first-class routes
app_ws(pattern, handler) uses the same
{param} matcher as your HTTP routes. When an
Upgrade request matches, your handler gets the raw socket and
decides: return 1 to own it — handshake, frames
and teardown are yours via std/websocket — or
0 to fall through. No match? A well-formed 404,
like any other route.
Templates that escape by default
{{title}} is HTML-escaped — always. Raw output is
the explicit exception ({{{html}}}), not the
default you forget to turn off. Conditionals, nested loops and
partials cover real pages without a DSL to learn.
- Escape by default — XSS-safe interpolation
- Logic —
{{#if}}/{{else}}, nested{{#each}} - Partials —
{{> header}}with a recursion guard
fn render_page(req: Request) -> Response { var ctx: Map = map_new() ctx.insert("title", "Deploys") ctx.insert("items", deploys) let tmpl: String = "<h1>{{title}}</h1>{{#each items}}<li>{{name}}</li>{{/each}}" return response_html(200, tpl_render(tmpl, ctx)) }
fn upload(req: Request) -> Response { let ctype: String = http_find_header(req.headers_flat, "Content-Type") let parts: Array = multipart_parse(req.body, ctype) let p: Array = parts[0] let filename: String = part_filename(p) let data: String = part_value(p) // bytes, NUL-safe return response_text(200, filename + ": " + int_to_string(data.length()) + " bytes") }
File uploads without a plugin
multipart/form-data parsing is built in and
binary-safe — a PNG with NUL bytes comes through intact.
Classic urlencoded forms land pre-parsed on
req.form. No body-parser middleware to configure,
no surprise defaults.
Middleware with one honest contract
A middleware is a function. Status 0 means
continue — and any headers it set still make it into the final
response. Any other status short-circuits the request right
there. Auth, CORS, rate limits: same three lines of shape.
- Built-in —
mw_cors+cors_configure - Hooks —
app_before/app_afterfor request IDs and logging - Sessions — cookie sessions backed by NyxKV, surviving redirects (Set-Cookie works on 3xx)
fn require_auth(req: Request) -> Response { let token: String = http_find_header(req.headers_flat, "Authorization") if token.length() == 0 { return response_json(401, "{\"error\":\"unauthorized\"}") } return Response { status: 0, headers_flat: [], body: "" } } // in main(): cors_configure("*", "GET, POST, OPTIONS", "Content-Type, Authorization") app_use(app, mw_cors) app_use(app, require_auth)
The guardrails are already on
The things you usually bolt on before going live are the defaults here — running on the servers that serve nyxlang.com itself.
Oversized bodies never reach you
Requests over the body cap (1 MiB default, configurable) get an automatic 413 and the connection closes — without reading the body. Draining an attacker-sized upload is a DoS vector; nyx-serve doesn't.
Conditional requests built in
app_static_cached adds Cache-Control and Last-Modified; apply_etag gives any response a weak ETag and collapses a matching If-None-Match into a 304.
Multi-threaded by default
serve_app(app, port, workers) runs a keep-alive worker pool — 9,971 req/s measured. One argument, not an ops project.
Pairs with the rest of the stack
Zero to a running API in three commands
The tutorial takes you from install to a deployed full-stack app — API, templates, sessions and a database.
$ curl -sSf https://nyxlang.com/install.sh | sh $ nyx init my-api && cd my-api $ nyx run my-api starting on port 3000 with 8 workers