Server parsing
The model is the artifact you share between client and server. Put it in a module both can import, and neither side has a parser of its own.
import { defineQueryModel, param } from "@queryweave/core";
export const products = defineQueryModel({ search: param.text({ trim: true }).optional(), page: param.integer({ min: 1 }).default(1), status: param.choice(["all", "active", "archived"]).default("all"),});Web-standard runtimes
Section titled “Web-standard runtimes”Anywhere Request exists — Deno, Bun, Cloudflare Workers, Vercel, Netlify, Hono, Nitro:
import { readRequestQuery } from "@queryweave/server";
import { products } from "./shared/products";
export async function GET(request: Request): Promise<Response> { const result = readRequestQuery(request, products); const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
return Response.json({ items: await findProducts(values), issues: result.issues, });}Node.js
Section titled “Node.js”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));});Behind a trusted proxy, opt in to forwarded headers explicitly:
const result = readNodeQuery(request, products, { trustForwardedHeaders: true });
result.ok; // true for /products?search=vue&page=2if (result.ok) result.value.page; // 2Express, Fastify, and friends
Section titled “Express, Fastify, and friends”No adapter package is needed. Express hands you the Node request itself, and a mounted router’s
stripped prefix does not matter because originalUrl is read:
app.get("/products", (request, response) => { const result = readNodeQuery(request, products); response.json(result.ok ? result.value : result.partial);});Fastify wraps the Node request; pass the wrapped one:
fastify.get("/products", (request, reply) => { const result = readNodeQuery(request.raw, products); return result.ok ? result.value : result.partial;});The recovery pattern
Section titled “The recovery pattern”Every server example above merges defaults with partial values:
const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
values.page; // always a usable number because page has a defaultresult.issues.map((issue) => issue.code); // keep stable codes for logs or diagnosticsDo this rather than returning a 400. A bad query usually comes from a stale link or a crawler, not
from an attack, and the useful response is the page with recovered values plus a note — not an
error. Keep result.issues for logs.
If a parameter genuinely must be present, declare it required and the decode will fail as a
whole, giving you a real decision point.
Canonical redirects
Section titled “Canonical redirects”Because encoding is deterministic, you can detect a non-canonical URL by comparing strings:
import { encodeQuery } from "@queryweave/server";
const canonical = encodeQuery(products, values);const incoming = new URL(request.url).search.replace(/^\?/u, "");
if (incoming !== canonical) { // ?page=1&status=all arrived; the canonical form is empty}QueryWeave does not perform the redirect. Server response contribution is an open decision, so the response is yours to write — and skipping the redirect is a legitimate choice, since decoding already recovered.
Building links back
Section titled “Building links back”import { createQueryUrl } from "@queryweave/server";
const nextPage = createQueryUrl(request.url, products, { ...values, page: values.page + 1 });nextPage.href; // managed page changes; an existing utm_source value survivesUnmanaged keys in the incoming URL are preserved after the managed ones, so tracking parameters survive.
- Request URL
- decode
- typed result
- canonical URL
A request is read once. There is no history to move through, so navigation is absent.
Type a query, press Enter.
Typed state
{
"page": 1,
"sort": "created_at",
"status": "all"
}Canonical URL
Valid/productsAsynchronous validation
Section titled “Asynchronous validation”// Web-standard Requestconst webResult = await readRequestQueryAsync(request, products);// Node IncomingMessageconst nodeResult = await readNodeQueryAsync(request, products);
[webResult.issues, nodeResult.issues]; // validation issues use the same QueryWeave shapeUse these when any parameter or the model uses an asynchronous refinement.
Testing it
Section titled “Testing it”readUrlQuery takes a string, so a server test needs no server:
import { readUrlQuery } from "@queryweave/server";
expect(readUrlQuery("/products?page=2", products)).toMatchObject({ ok: true, value: { page: 2 },});A relative string is resolved against relativeUrlBase, which is exported for exactly this.