Introduction
QueryWeave is a framework-independent, type-safe URL state engine. You define your query parameters once and reuse that definition everywhere the query appears: in the browser, on the server, in a router, or in a test.
The problem
Section titled “The problem”A URL query is a string, a navigation event, a link someone can share, and input your program did not write, all at once. Applications often end up describing it more than once:
- a parser in the client component that reads
pageas a number, - a second parser on the server that reads it again,
- a serializer somewhere else that writes it back,
- and a default value repeated in all three.
Those descriptions drift. The server accepts page=0, the client does not. A link built by hand
produces ?sort=name&page=1 while the application produces ?page=1&sort=name, and the two cache
differently. None of this throws an error, so it tends to go unnoticed.
What QueryWeave does
Section titled “What QueryWeave does”A query model describes your query parameters once. Each parameter defines how to decode and encode its value, so parsing and serialization cannot drift apart.
import { defineQueryModel, param } from "@queryweave/core";
const products = defineQueryModel({ search: param.text().optional(), page: param.integer({ min: 1 }).default(1), sort: param.choice(["name", "created_at", "price"]).default("created_at"),});The model is a plain value that knows nothing about browsers, reactivity, or navigation. The same object decodes a request on the server, encodes a link, binds to a component, and runs in a test.
The path a query takes through the model, stage by stage.
- Raw query — ?search=vue&page=2
- decode()
- Typed state — { search: "vue", page: 2 }
- encode()
- Canonical query — search=vue&page=2
Raw query → decode(). decode() → Typed state. Typed state → encode(). encode() → Canonical query.
What QueryWeave guarantees
Section titled “What QueryWeave guarantees”- A value equal to its declared default is omitted from canonical output. Two states that mean the same thing produce the same URL.
- An invalid value recovers to its default or to
undefined, and reports an issue.?page=abcdecodes to page1with aninvalidissue onpage. Decoding does not throw. - Repeated keys survive.
?tag=a&tag=bdecodes to both values. The query is not flattened toRecord<string, string>. - An update encodes once, writes the URL at most once, and notifies subscribers at most once.
A
transaction()can change several fields in its callback and still commits once, at the end. If the result matches the URL already there, nothing is written. - Keys your model does not define stay in the URL. If the current URL is
?page=2&utm_source=newsletterand your model only managespage, updatingpagewrites?page=3&utm_source=newsletter. You can start managing some parameters without modelling all of them.
Not included
Section titled “Not included”- A debounce. A runtime can throttle its writes, which holds a burst and bounds how far a shared URL lags behind the reader; a quiet-period delay stays a call-site concern. The reasoning is in ADR 0010.
- A validation library. Bring your own validator through Standard Schema.
Where to go next
Section titled “Where to go next”- Installation — the packages, and which ones you need.
- Quick start — a model, a decode, an encode, and a runtime.
- Why QueryWeave — the reasoning behind a model-first API.
- URL state as a domain — the concepts in order.