Network servers that fit where Node.js doesn't.
qjs-lws binds libwebsockets into QuickJS: HTTP/1.1 and HTTP/2, WebSocket, raw TCP and UDP, TLS — driven by QuickJS's own event loop, with a JavaScript layer that looks like the web platform you already know.
import { serve, Response } from './lib/serve.js';
serve({
port: 8080,
websocket: {
open(ws) { ws.subscribe('chat'); },
message(ws, data) { ws.publish('chat', data); },
},
fetch(req, server) {
const { pathname } = new URL(req.url);
if(pathname === '/ws' && server.upgrade(req))
return;
return new Response('hello from qjs-lws');
},
});Why it exists
A production-grade C networking stack and a very small interpreter, joined so that neither one has to pretend to be the other.
No service loop
The binding hooks libwebsockets' pollfd callbacks straight into QuickJS's own
os.setReadHandler and os.setWriteHandler. No
lws_service() spun from JS, no polling tax — your script's normal event
loop drives the stack. An optional epoll(7) backend collapses it to a
single fd.
Every role, one context
HTTP client and server, WebSocket client and server, raw TCP and UDP, static-file
mounts, redirects, and TLS with peer-certificate introspection — all from one
LWSContext, on one port if you want it that way.
APIs you already know
fetch(), WebSocket, URL/URLSearchParams,
Headers, Request, Response, FormData,
WHATWG Streams, AbortController, TCPSocket. Written from the
specs, not approximated.
Bun-shaped serve()
serve({ fetch, websocket }) with server.upgrade(), WebSocket
pub/sub topics, chunked streaming and backpressure — close enough to Bun that most
handlers port across unchanged.
Routing and middleware
An Express-compatible app/Router layer when you want one:
path params, method dispatch, sub-routers, sessions, multipart bodies, MIME types.
Optional — the primitives underneath stay reachable.
Drop to C-level control
The wrappers are a convenience, never a wall. Every callback reason libwebsockets
emits is reachable as an onEventName handler, with the raw
wsi in hand.
Built for the small end
Where a Node or Bun runtime is the largest thing on the device, and the job is one well-defined piece of networking.
Embedded & appliance firmware
A scriptable control plane on hardware with a flash budget: config UI, telemetry push over WebSocket, firmware upload through the native multipart parser.
Small utility servers
Health endpoints, webhook receivers, metrics scrapers, LAN dashboards — the
services that never justified a node_modules tree.
Gateways & proxies
Terminate TLS, translate protocols, bridge WebSocket to raw TCP. Mixed HTTP and raw listeners on one vhost make protocol-sniffing front doors straightforward.
Two altitudes, same stack
Native — you hold the wsi
Protocol handlers map one-to-one onto struct lws_protocols.
import { createServer, LWSMPRO_NO_MOUNT } from 'lws';
createServer({
port: 8080,
vhostName: 'localhost',
mounts: [{
mountpoint: '/chat',
protocol: 'chat',
originProtocol: LWSMPRO_NO_MOUNT,
}],
protocols: [{
name: 'chat',
onEstablished(wsi) { console.log('open', wsi.peer?.host); },
onReceive(wsi, data) { wsi.write(data); },
onClosed(wsi) { console.log('close'); },
}],
});Web-shaped — you hold a Request
The same connection, wrapped in the APIs a browser would hand you.
import { fetch } from './lib/fetch.js';
const res = await fetch('https://example.com/api', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ hello: 'world' }),
});
console.log(res.status, res.headers.get('content-type'));
console.log(await res.json());Get it running
CMake builds the module and, if you let it, the vendored libwebsockets too.
git clone --recursive https://github.com/rsenn/qjs-lws.git
cd qjs-lws
mkdir build && cd build
cmake -DDEBUG_OUTPUT=OFF -DDO_TESTS=ON ..
make -j
# lws.so lands in build/; point QuickJS at it
qjs -I ./build ../tests/unittests/test-lwscontext.js- Dependencies: a C compiler, CMake ≥ 3.5, a QuickJS install, and libwebsockets (vendored by default).
- Platforms: Linux and macOS natively; Windows via MinGW.
- Licence: MIT.
Where it stands
Honest status: qjs-lws is in daily use and the native API is stable, but it hasn't cut a versioned release yet — track the changelog and pin a commit. The web-platform layer is a deliberate subset: no CORS, cache, or service-worker semantics, because this runs server-side. Known gaps are written down rather than papered over — see the API compatibility report and the BUGS file.
Worth a look next
- Bun-compatible
serve()— upgrades, pub/sub, streaming - Event-loop integration — how the pollfd hook works
- Twelve worked examples — one per role
- examples/ — a crawler, a DNS server, proxies, a WebSocket chat