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 the only application state that is simultaneously a string, a user-visible artifact, a navigation event, a shareable link, and an input from outside your program. Most codebases 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. Nothing is broken enough to fail loudly, and everything is slightly wrong.
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"),});That model is a plain value. It has no environment, no reactivity, and no navigation policy. Decode it on a server, encode it into a link, bind it to a component, or assert against it in a test — the model itself does not change.
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. It never disappears silently and never throws. - Repeated keys survive. A query is never reduced to
Record<string, string>, because?tag=a&tag=bcarries meaning. - One
update()means one encode, one navigation write, and one subscriber notification. Callingruntime.update()orruntime.transaction()does not encode the URL twice, create duplicate history entries, or fire your listener multiple times for that single call. Atransaction()can change several fields in its callback, but the runtime still commits once at the end. - 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 do not have to model every query parameter before you can manage some of them.
What QueryWeave does not do
Section titled “What QueryWeave does not do”- It does not schedule, throttle, coalesce, or cancel transitions. Every operation is applied immediately. This is deliberate and recorded in ADR 0004.
- It does not bundle a validation library. Validators join through Standard Schema.
- It does not decide navigation policy inside a codec, and it does not let an adapter decode.
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.