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
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) }
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)
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 // ahora el socket es nuestro } // en main(): app_ws("/ws/chat/{room}", chat_handler)
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.
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
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, seguro con NUL return response_text(200, filename + ": " + int_to_string(data.length()) + " bytes") }
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.
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_afterpara request IDs y logging - Sesiones — sesiones por cookie respaldadas por NyxKV, que sobreviven redirects (Set-Cookie funciona en 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: "" } } // en main(): cors_configure("*", "GET, POST, OPTIONS", "Content-Type, Authorization") app_use(app, mw_cors) app_use(app, require_auth)
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.
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.
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.
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.
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.
$ 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