Skip to content

Repository files navigation

@askrjs/node

CI npm version

Run an @askrjs/server application on Node.js. The adapter translates Node HTTP messages at the boundary while the application continues to use Web Request and Response objects.

Install

npm install @askrjs/server @askrjs/node

Create a Node handler

import { createServer } from "node:http";
import { createServerApp, json } from "@askrjs/server";
import { createNodeHandler } from "@askrjs/node";

const app = createServerApp({
  routes: [{ path: "/health", handler: () => json({ status: "ok" }) }],
});

createServer(createNodeHandler(app, { baseUrl: "http://localhost:3000" })).listen(3000);

createNodeHandler also works as Connect middleware because it accepts an optional next callback. It preserves streaming bodies, repeated Set-Cookie headers, aborts, backpressure, status text, and HEAD responses. next receives adapter failures; application responses, including 404, remain owned by the ServerApp and do not fall through.

Every handler must have a trusted URL boundary. Pass baseUrl when the external origin is fixed, or allowedHosts when the request Host determines the origin. Host names are canonicalized and compared case-insensitively; entries without a port allow that host on any port, while entries with a port require that exact authority. Absolute-form request targets must retain the trusted origin, and ambiguous network-path targets are rejected.

Listen directly

import { listen } from "@askrjs/node";

const server = await listen(app, {
  port: 3000,
  requestTimeout: 120_000,
  headersTimeout: 60_000,
  keepAliveTimeout: 5_000,
});
server.close();

Pass an AbortSignal to integrate shutdown with your process lifecycle.

Enable the built-in ws transport with websocket: true. It defaults to a 1 MiB maximum message payload with compression disabled; pass websocket: { closeTimeout, maxPayload, maxRejectionBodyBytes, perMessageDeflate } to override those settings. Rejected upgrade bodies are capped at 64 KiB by default. Shutdown gives clients five seconds to complete the close handshake by default, then terminates any remaining connections. Upgrades require an Origin. The request origin is allowed by default; set websocket.allowedOrigins to a canonical allowlist when trusted browser origins differ from the application origin.

router.ws("/echo", (socket) => {
  socket.onMessage((message) => socket.send(message));
});

const server = await listen(createServerApp({ router }), { websocket: true });

Route matching, authentication, and middleware complete before the handshake. Rejected upgrades preserve the application response, and shutdown closes active sockets.

Serve a production application

import { serve } from "@askrjs/node";

const running = await serve(app, {
  port: 3000,
  assets: { root: "./dist/client" },
});

await running.close();

serve handles static assets and closes both the HTTP server and the application during shutdown. When an asset root is configured, extension-bearing GET and HEAD paths are reserved for static files: missing files return 404 without falling through to application routing, source maps are not served, and resolved files must remain inside the configured root. Fingerprinted files under /assets/ receive immutable caching; other files receive no-cache. Both listen and serve bind to 127.0.0.1 by default. A non-loopback host also requires allowPublicBind: true so public exposure is explicit. Public listeners should declare every external host name through allowedHosts; the bind host and localhost remain allowed automatically:

await serve(app, {
  host: "0.0.0.0",
  allowPublicBind: true,
  allowedHosts: ["app.example.com"],
});

listen and serve fail before binding when their AbortSignal is already aborted. Later aborts start shutdown. serve().close() is idempotent, waits for HTTP and WebSocket closure, and then closes the application exactly once.

MCP over stdio

import { connectMcpStdio } from "@askrjs/node/mcp";

const connection = connectMcpStdio(mcp, { dependencies });
await connection.closed;

Protocol messages use stdin/stdout; diagnostics remain isolated on stderr. Authentication may be provided directly or resolved from the process environment for each message. Closing stdin, calling connection.close(), or aborting its signal detaches the transport, cancels active requests, terminates the MCP session, and prevents late protocol output.

About

Node.js HTTP adapter for the transport-neutral Askr server.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages