Skip to content
Runtime
Framework

Node.js

Adapter

Node request primitives bridged into the server helpers.

Package
@queryweave/node
Runtime
Node.js
Depends on
@queryweave/core, @queryweave/server
Framework required
No
InstallQuick examplePackage: @queryweave/node

One thing: turning a Node IncomingMessage into an absolute URL. Everything after that is delegated to @queryweave/server, so decoding lives in exactly one place.

Decoding, validation, defaults, navigation, and any HTTP framework. Express, Fastify, NestJS, and Hono are not dependencies and never will be — they all expose a request object this package can read.

Request-scoped source — reads once, never navigates

Request-scoped source — reads once, never navigates

  1. IncomingMessage (incoming)
  2. QuerySource (read-only)
  3. QueryModel.decode() (typed result)

IncomingMessage → QuerySource. QuerySource → QueryModel.decode().

Resolve an absolute URL, then hand the query to the same decoder the browser uses.
Terminal window
pnpm add @queryweave/core @queryweave/server @queryweave/node
import { createServer } from "node:http";
import { readNodeQuery } from "@queryweave/node";
createServer((request, response) => {
const result = readNodeQuery(request, products);
const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ values, issues: result.issues }));
}).listen(3000);

A Node request carries a path, not an absolute URL, so the authority has to come from somewhere:

import { resolveNodeRequestUrl } from "@queryweave/node";
resolveNodeRequestUrl(request);
// http://<host header>/products?page=2
resolveNodeRequestUrl(request, { host: "example.com", protocol: "https" });
// https://example.com/products?page=2

The order of precedence is: explicit options, then forwarded headers if you opted in, then the host header, then the fallback host queryweave.invalid.

import { createNodeQuerySource } from "@queryweave/node";
const source = createNodeQuerySource(request);
source.read(); // "?page=2"
source.request; // the original IncomingMessage
const result = await readNodeQueryAsync(request, products);
const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
const rows = await searchProducts(values);
result.issues; // keep normalized validation issues for logging

Same rule as everywhere else: use the asynchronous form when a parameter or the model uses an asynchronous refinement.

  • A request with no URL resolves to /.
  • A comma-separated forwarded header uses the first entry, trimmed.
  • An empty header value is ignored rather than producing an empty host.
  • The fallback host is queryweave.invalid, chosen because .invalid can never resolve.

tests/node/node.test.ts covers URL resolution, header precedence, the forwarded-header opt-in, and decoding. The node consumer fixture runs the packed archive against a real node:http server.