Todo el framework web. Un binario de 170KB.

Rutas, JSON, WebSockets, plantillas, uploads, sesiones y protecciones de producción — compilado a un único binario estático con cero dependencias. Sin node_modules. Sin runtime que instalar en el servidor. Este es el framework que corre nyxlang.com.

$ curl -sSf https://nyxlang.com/install.sh | sh

src/main.nx — una API JSON completa
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
un binario estático
0
dependencias en runtime
9,971 req/s
keep-alive, multi-hilo
3 comandos
de cero a una API corriendo
Rutas y JSON

Los handlers son funciones comunes

Una ruta asocia un patrón a una función. Los parámetros de ruta con {id}, el query string, las cookies y el body ya parseado llegan separados en el Request: lees campos, devuelves un Response. Nada se esconde detrás de decoradores o globales mágicos.

  • Parámetros de ruta — req.params.get("id")
  • JSON de entrada — req_json(req) te da un Map
  • JSON de salida — response_json_map(201, data)
POST /notes — lee JSON, responde 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 — rutas WebSocket con {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  // ahora el socket es nuestro
}

// en main():
app_ws("/ws/chat/{room}", chat_handler)
Nuevo en v0.3

Los WebSockets son rutas de primera clase

app_ws(pattern, handler) usa el mismo matcher {param} que tus rutas HTTP. Cuando llega un Upgrade que matchea, tu handler recibe el socket crudo y decide: devuelve 1 para adueñarte de él — handshake, frames y cierre son tuyos vía std/websocket — o 0 para dejarlo pasar. ¿Sin match? Un 404 bien formado, como cualquier otra ruta.

Nuevo en v0.3

Plantillas que escapan por defecto

{{title}} se escapa como HTML — siempre. La salida cruda es la excepción explícita ({{{html}}}), no el default que te olvidas de apagar. Condicionales, loops anidados y parciales cubren páginas reales sin un DSL que aprender.

  • Escape por defecto — interpolación a prueba de XSS
  • Lógica — {{#if}}/{{else}}, {{#each}} anidado
  • Parciales — {{> header}} con guarda de recursión
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 — uploads binary-safe
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, seguro con NUL

    return response_text(200, filename + ": " +
        int_to_string(data.length()) + " bytes")
}
Nuevo en v0.3

Subida de archivos sin plugins

El parseo de multipart/form-data viene incluido y es binary-safe — un PNG con bytes NUL llega intacto. Los forms urlencoded clásicos llegan ya parseados en req.form. Sin middleware de body-parser que configurar, sin defaults sorpresa.

Composición

Middleware con un contrato honesto

Un middleware es una función. Status 0 significa continuar — y cualquier header que haya puesto igual llega a la respuesta final. Cualquier otro status corta el request ahí mismo. Auth, CORS, rate limits: la misma forma de tres líneas.

  • Incluido — mw_cors + cors_configure
  • Hooks — app_before / app_after para request IDs y logging
  • Sesiones — sesiones por cookie respaldadas por NyxKV, que sobreviven redirects (Set-Cookie funciona en 3xx)
auth en nueve líneas
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: "" }
}

// en main():
cors_configure("*", "GET, POST, OPTIONS", "Content-Type, Authorization")
app_use(app, mw_cors)
app_use(app, require_auth)
Defaults de producción

Las protecciones ya están puestas

Lo que normalmente atornillas antes de salir a producción acá son los defaults — corriendo en los servidores que sirven nyxlang.com.

413 Payload Too Large

Los bodies gigantes nunca te llegan

Los requests sobre el límite de body (1 MiB por defecto, configurable) reciben un 413 automático y la conexión se cierra — sin leer el body. Drenar un upload del tamaño de un ataque es un vector de DoS; nyx-serve no lo hace.

ETag / 304

Requests condicionales incluidos

app_static_cached agrega Cache-Control y Last-Modified; apply_etag le da a cualquier respuesta un ETag débil y colapsa un If-None-Match que matchee a un 304.

keep-alive × N workers

Multi-hilo por defecto

serve_app(app, port, workers) corre un pool keep-alive de workers — 9,971 req/s medidos. Un argumento, no un proyecto de ops.

Ecosistema

Se combina con el resto del stack

De cero a una API corriendo en tres comandos

El tutorial te lleva desde la instalación hasta una app full-stack desplegada — API, plantillas, sesiones y una base de datos.

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