The Node server
serve(handler, options) starts Node's http server around any object with fetch(request, platform).
serve(handler, options) starts Node's http server around any object with fetch(request, platform). It turns
each request into a Web Request with a streaming body and a signal that aborts when the client disconnects, streams
the response back with backpressure, compresses text responses, answers static files itself, and shuts down gracefully.
dusk start runs the production build with the Node adapter, and dist/client is served as static files. To set the
options yourself, write a serve.mjs file like the one below and run it with node serve.mjs.
import { fileURLToPath } from "node:url";
import { serve } from "@shadow-js/dusk/node";
import { handler } from "./dist/server/entry.js";
const server = await serve(handler, {
port: Number(process.env.PORT ?? 3000),
host: process.env.HOST ?? "127.0.0.1",
static: { dir: fileURLToPath(new URL("./dist/client/", import.meta.url)) },
});
console.log(`listening on ${server.url}`);
The same file in TypeScript. dist/server/entry.js has no type declarations, so the import is typed by hand:
import { fileURLToPath } from "node:url";
import { serve } from "@shadow-js/dusk/node";
import type { Handler } from "@shadow-js/dusk/server";
const entry = (await import(new URL("./dist/server/entry.js", import.meta.url).href)) as {
handler: Handler;
};
const server = await serve(entry.handler, {
port: Number(process.env["PORT"] ?? 3000),
host: process.env["HOST"] ?? "127.0.0.1",
static: { dir: fileURLToPath(new URL("./dist/client/", import.meta.url)) },
});
console.log(`listening on ${server.url}`);
Running it on the built scratch app prints listening on http://127.0.0.1:4401 with PORT=4401. The server listens on
127.0.0.1 unless host says otherwise; in a container use 0.0.0.0 (the Dockerfile below sets HOST). An option
name that serve does not know throws a TypeError that suggests the closest one.
ServeOptions |
Default | Meaning |
|---|---|---|
port |
3000 |
0 picks a free port; server.port reports it |
host |
"127.0.0.1" |
interface to listen on |
static |
none | false or StaticOptions; served before the handler |
compress |
on | false, or CompressOptions; br, zstd (where node:zlib has it) or gzip by Accept-Encoding, flushed per chunk so streamed pages stay progressive |
earlyHints |
true |
send 103 Early Hints for page requests (Early Hints) |
trustProxy |
false |
believe X-Forwarded-Proto and X-Forwarded-Host (proxy) |
gracefulShutdownMs |
10000 |
how long a shutdown waits for requests in flight |
shutdownSignals |
["SIGTERM", "SIGINT"] |
signals that start a shutdown; false registers none |
signalTarget |
process |
where the signal listeners are registered |
exitOnSignal |
true |
call exit(0) after a signal-triggered shutdown |
exit |
process.exit |
called with the exit code |
headersTimeoutMs |
30000 |
time to receive the request headers |
requestTimeoutMs |
120000 |
time to receive the whole request |
keepAliveTimeoutMs |
5000 |
idle time before a keep-alive connection closes |
maxHeaderBytes |
16384 |
largest header block |
workers |
none | n or "auto": run that many server processes that share the port (Using every core); absent or 1 is one process |
supervise |
see below | restart delays, the crash-loop breaker and the restart signal of the worker pool |
tls |
none | https server options; server.url then starts with https:// |
onError |
console.error |
failures that cannot reach the client (a body that broke mid-stream) |
serve returns a ServerHandle: role, port, host, url, the Node server, draining and close(). Static files are
answered before the handler runs, so they do not carry the handler's security headers, host check or request id. If
that matters, serve dist/client from a CDN or the proxy and give the proxy the headers.