Skip to content
QueryWeave

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

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

  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 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.

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 /.
  • An absolute-form request line (GET http://origin.test/p) is used as given; explicit host and protocol options 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 .invalid can never resolve.
  • Fastify and Koa hand you a wrapper; pass request.raw (Fastify) or ctx.req (Koa), which is the underlying IncomingMessage. Express and Hono’s Node adapter pass it through directly.