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
This package turns a Node IncomingMessage into an absolute URL, and that is all it does.
Everything after that is handed to @queryweave/server, so decoding lives in
one place. No HTTP framework is a dependency, and none will be: Express, Fastify, NestJS, and Hono all
expose a request object this package can read.
Request-scoped source — reads once, never navigates
Request-scoped source — reads once, never navigates
- IncomingMessage (incoming)
- QuerySource (read-only)
- QueryModel.decode() (typed result)
IncomingMessage → QuerySource. QuerySource → QueryModel.decode().
Install
Section titled “Install”pnpm add @queryweave/core @queryweave/server @queryweave/nodeExample
Section titled “Example”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);Resolving the URL
Section titled “Resolving the URL”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=2The order of precedence is: explicit options, then forwarded headers if you opted in, then the
host header, then the HTTP/2 :authority pseudo-header, then the fallback host
queryweave.invalid. The scheme follows x-forwarded-proto (opt-in), :scheme, then the socket:
https when it is encrypted, http otherwise.
The path comes from originalUrl when a framework set it — Express strips a mounted router’s
prefix from url and keeps the full path there — and from url otherwise. A target such as
//evil.example/p stays a path on the request’s own host rather than becoming a different origin,
and a Host header that cannot form a URL falls back instead of throwing.
A request-scoped source
Section titled “A request-scoped source”import { createNodeQuerySource } from "@queryweave/node";
const source = createNodeQuerySource(request);source.read(); // "?page=2"source.request; // the original IncomingMessageAsynchronous validation
Section titled “Asynchronous validation”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 loggingSame rule as everywhere else: use the asynchronous form when a parameter or the model uses an asynchronous refinement.
Edge cases
Section titled “Edge cases”- A request with no URL resolves to
/. - An absolute-form request line (
GET http://origin.test/p) is used as given; explicithostandprotocoloptions still override its authority. - 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.invalidcan never resolve. - Fastify and Koa hand you a wrapper; pass
request.raw(Fastify) orctx.req(Koa), which is the underlyingIncomingMessage. Express and Hono’s Node adapter pass it through directly.