Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

serveNode

Serves a Meeko application with Node's HTTP server, its Socket channels with ws, graceful shutdown on signals and the process failure policy.

adapter Since 1.0.0 @meekojs/meeko/node Source code
ts
import { serveNode } from "@meekojs/meeko/node";

serveNode(app, options) starts a node:http server for a createServerApp application: it converts each request with toNodeListener, connects Socket channels to the same server with attachNodeWebSocketServer (if the application has them) and, with handleSignals, shuts down gracefully on SIGINT and SIGTERM and applies processErrors. This is what the Node output of meeko build runs (.meeko/output/node/index.mjs, started by meeko start).

Write it manually only for a custom host; with the node adapter, meeko build already generates the entry point.

Usage#

ts
import { createServerApp } from "@meekojs/meeko/server";
import { serveNode } from "@meekojs/meeko/node";

const app = await createServerApp({ manifest, load, runtime, production: true });
const server = await serveNode(app, { host: "0.0.0.0", port: 3000, publicOrigin: "https://example.com", handleSignals: true });
console.log(`Listening on ${server.url}`);

Type#

ts
function serveNode(app: ServerApp, options?: ServeNodeOptions): Promise<NodeServer>

ServeNodeOptions

ts
interface ServeNodeOptions {
	readonly host?: string
	readonly port?: number
	readonly publicOrigin?: string
	readonly platform?: ServerPlatform
	readonly onError?: (error: unknown, requestId: string) => void
	readonly handleSignals?: boolean
	readonly shutdownTimeout?: number
}

NodeServer

ts
interface NodeServer {
	readonly server: Server
	readonly url: string
	close(): Promise<void>
}

Parameters#

serveNode(app, options?):

Option Type Default Description
host string "127.0.0.1" Listening interface. "0.0.0.0" accepts remote traffic.
port number 4173 Port; 0 chooses an available one.
publicOrigin string the application's Public origin used to construct URLs and check Origin for HTTP and Socket, such as https://example.com behind a TLS proxy.
platform ServerPlatform Node with process.env Each request's platform: name, env and waitUntil.
onError (error, requestId) => void the application's Observer for transport failures.
handleSignals boolean false Shuts down on SIGINT and SIGTERM. Since 1.3, also applies processErrors.
shutdownTimeout number 10000 Milliseconds open responses (a stream) can continue after close() before their connections are terminated.

Return value#

A promise with a NodeServer:

Member Description
server Node's http.Server.
url The local URL with the listening address, such as http://127.0.0.1:4173 (with 0.0.0.0, the loopback address). Unchanged in 1.7.1: meeko start and Node output display http://localhost:4173, but url remains the actual address and Socket identity without publicOrigin.
close() Closes, in order, Socket peers, HTTP (with shutdownTimeout), waitUntil tasks and the application. It is idempotent.

If the server starts but Socket attachment fails, it releases the port and closes the application before rejecting.

Examples#

Production with meeko start#

ts
// meeko.config.ts
import { defineMeekoConfig } from "@meekojs/meeko/framework";

export default defineMeekoConfig({ server: { adapter: "node", host: "0.0.0.0", port: 3000 } });
sh
npm run build
MEEKO_PUBLIC_ORIGIN=https://tienda.example.com MEEKO_REDIS_URL=rediss://… MEEKO_TRUST_PROXY=1 npm start

MEEKO_HOST and MEEKO_PORT (or PORT) override server.host and server.port. The .meeko/output/node/ folder does not read app/ or server/: it only needs node_modules, including @meekojs/meeko.

A container#

dockerfile
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY .meeko/output/node ./.meeko/output/node
ENV MEEKO_HOST=0.0.0.0 MEEKO_PORT=3000
EXPOSE 3000
CMD ["node", ".meeko/output/node/index.mjs"]

The generated entry point calls serveNode with handleSignals: true: the orchestrator's SIGTERM closes Socket, HTTP, waitUntil tasks and the runtime, in that order.

An available port in tests#

ts
const node = await serveNode(app, { port: 0 });
try {
  const response = await fetch(`${node.url}/api/health`);
  assert.equal(response.status, 200);
} finally {
  await node.close();
}

Common mistakes#

Listening on 127.0.0.1 inside a container#

Incorrectsh
docker run -p 3000:4173 tienda   # without MEEKO_HOST

There is no error: the server listens on the container's loopback, and the published port cannot reach it. The client sees a refused or closed connection.

Correctsh
docker run -p 3000:3000 -e MEEKO_HOST=0.0.0.0 -e MEEKO_PORT=3000 tienda

An invalid MEEKO_TRUST_PROXY#

Incorrectsh
MEEKO_TRUST_PROXY=yes node .meeko/output/node/index.mjs

Startup stops with TypeError: MEEKO_TRUST_PROXY must be true, false or the number of trusted proxies (1-99), or cloudflare, not "yes": serveNode checks the variable before listening, not on every request.

Correctsh
MEEKO_TRUST_PROXY=1 node .meeko/output/node/index.mjs

An out-of-range port#

Incorrectts
await serveNode(app, { port: 70000 });

RangeError: Invalid port: 70000. In meeko.config.ts, an out-of-range server.port stops meeko build with server.port debe ser un entero entre 0 y 65535.

Correctts
await serveNode(app, { port: Number(process.env.MEEKO_PORT ?? 4173) });

Behind a proxy without publicOrigin#

Incorrectsh
# Caddy terminates TLS at https://tienda.example.com and forwards to http://127.0.0.1:4173
node .meeko/output/node/index.mjs

API and RPC mutations (since 1.8, also without a session) respond with 403 INVALID_ORIGIN, and browser Socket upgrades with 403: the application compares Origin with its own address, not the public one.

Correctsh
MEEKO_PUBLIC_ORIGIN=https://tienda.example.com node .meeko/output/node/index.mjs

Notes#

  • Node 24. The adapter uses node:http and Web Streams; Set-Cookie headers are sent separately.
  • Socket. It uses the ws package (loaded only if the application has channels), with frames up to socket.maxMessageBytes (64 KiB by default), no compression and a 30 s heartbeat. Binary messages are closed with 1003.
  • Assets. The application serves them from its manifest (createNodeAssetReader): it never reads a disk path not listed there.
  • Client IP. context.clientAddress and clientAddress(request) are the remote socket; behind a proxy, MEEKO_TRUST_PROXY=1 takes the entry the proxy added to X-Forwarded-For, and behind Cloudflare Tunnel (since 1.8), MEEKO_TRUST_PROXY=cloudflare takes CF-Connecting-IP if the connection comes from networks in MEEKO_TRUSTED_PROXIES.
  • CSP and headers (since 1.7). server.contentSecurityPolicy and server.headers from meeko.config.ts apply to every production response, with a nonce per SSR response and hashes for prerendered ones.
  • Request-target. A path starting with // is always on the same origin, never another host.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/adapters/servenode.md