Skip to main content

Serving HTTP

txiki.js includes a high-performance HTTP/HTTPS server with WebSocket support. The quickest way to stand one up is the tjs serve command, which runs a module that default-exports a fetch handler.

tjs serve

tjs serve app.js

The module must export default an object with a fetch method (and, optionally, websocket handlers):

// app.js
export default {
fetch(request) {
const url = new URL(request.url);

return new Response(`Hello World!\nYou requested: ${url.pathname}\n`);
},
};

The handler receives a standard Request and returns a Response (or a promise resolving to one). On startup, the server prints the address it is listening on:

Listening on http://localhost:8000/

Options

OptionDescription
-p, --port PORTPort to listen on (default 8000)
--tls-cert FILEPath to a TLS certificate PEM file (enables HTTPS)
--tls-key FILEPath to the matching TLS private key PEM file

--tls-cert and --tls-key must be supplied together.

The request context

The fetch handler receives a second argument — a context object — with the server instance and the remote client address:

export default {
fetch(request, { server, remoteAddress }) {
console.log(`request from ${remoteAddress}`);

return new Response('ok');
},
};

server is used to upgrade requests to WebSockets (see below).

WebSockets

To accept WebSocket connections, call server.upgrade(request) from the fetch handler when an upgrade is requested, and provide a websocket object with event handlers:

// ws-echo-server.js — run with: tjs serve ws-echo-server.js
export default {
fetch(request, { server }) {
if (request.headers.get('upgrade') === 'websocket') {
server.upgrade(request);

return;
}

return new Response('This is a WebSocket server.\n');
},
websocket: {
open(ws) {
console.log('Client connected');
},
message(ws, data) {
ws.sendText(`echo: ${data}`);
},
close(ws, code, reason) {
console.log(`Client disconnected: ${code} ${reason}`);
},
},
};

server.upgrade() must be called synchronously inside fetch, and the handler should return without a Response once it does. The connection object passed to the handlers exposes:

MemberDescription
ws.sendText(string)Send a text message
ws.sendBinary(Uint8Array)Send a binary message
ws.close(code?, reason?)Close the connection
ws.dataArbitrary data associated at upgrade time via server.upgrade(request, { data })

HTTPS

Generate a self-signed certificate for local testing:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'

Then serve over HTTPS:

tjs serve --tls-cert cert.pem --tls-key key.pem app.js

The startup log reflects the https:// scheme.

Programmatic API

tjs serve is a thin wrapper around tjs.serve(), which you can call directly for full control — custom bind address, TLS configuration as in-memory PEM strings, graceful shutdown, and more:

const cert = new TextDecoder().decode(await tjs.readFile('cert.pem'));
const key = new TextDecoder().decode(await tjs.readFile('key.pem'));

const server = tjs.serve({
port: 8443,
tls: { cert, key },
fetch(request) {
return new Response('Hello HTTPS!\n');
},
});

console.log(`Listening on https://localhost:${server.port}/`);

A shorthand form accepts just the fetch handler:

const server = tjs.serve((request) => new Response('Hello!'));

The returned Server exposes server.port and await server.close(). It is also async-disposable, so await using server = tjs.serve(...) closes it automatically at the end of the scope. See ServeOptions for the full set of options, including the TlsOptions ca, passphrase, and requestCert fields for mutual TLS.

HTTP/2

HTTPS connections negotiate HTTP/2 automatically via ALPN: a client that advertises h2 gets HTTP/2, otherwise the connection falls back to HTTP/1.1. The request handler is identical either way.

To restrict which protocols the server offers, set tls.alpn to a list in preference order. For example, alpn: ['h2'] requires HTTP/2 — a client that cannot speak it has no protocol in common and fails the TLS handshake instead of downgrading:

tjs.serve({
tls: { cert, key, alpn: ['h2'] },
fetch(request) {
return new Response('HTTP/2 only\n');
},
});