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

src/main.nx — a complete JSON API
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)
}
170KB
one static binary
0
runtime dependencies
9,971 req/s
keep-alive, multi-threaded
3 commands
from zero to running API
Routing & JSON

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)
POST /notes — read JSON, answer JSON
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)
}
app_ws — WebSocket routes with {params}
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)
New in v0.3

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.

New in v0.3

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
tpl_render — if / each / escape
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))
}
multipart_parse — binary-safe uploads
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")
}
New in v0.3

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.

Composition

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_after for request IDs and logging
  • Sessions — cookie sessions backed by NyxKV, surviving redirects (Set-Cookie works on 3xx)
auth in nine lines
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)
Production defaults

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.

413 Payload Too Large

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.

ETag / 304

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.

keep-alive × N workers

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.

Ecosystem

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.

terminal
$ 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