Skip to content
Runtime
Framework

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.

shared/products.ts
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"),
});

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,
});
}
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=2
if (result.ok) result.value.page; // 2

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;
});

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 default
result.issues.map((issue) => issue.code); // keep stable codes for logs or diagnostics

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

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.

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 survives

Unmanaged keys in the incoming URL are preserved after the managed ones, so tracking parameters survive.

Server mode: a request URL comes in, is decoded, and produces a canonical form. There is no history, so there are no history controls.

Products

6 matching

  • Edge runtime handbookactive$59
  • Node.js request toolkitactive$39
  • Nuxt deployment guidearchived$19
  • 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
/products

    // Web-standard Request
    const webResult = await readRequestQueryAsync(request, products);
    // Node IncomingMessage
    const nodeResult = await readNodeQueryAsync(request, products);
    [webResult.issues, nodeResult.issues]; // validation issues use the same QueryWeave shape

    Use these when any parameter or the model uses an asynchronous refinement.

    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.