Skip to content
Runtime
Framework

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.

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

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.

  1. Raw query — ?search=vue&page=2
  2. decode()
  3. Typed state — { search: "vue", page: 2 }
  4. encode()
  5. Canonical query — search=vue&page=2

Raw query → decode(). decode() → Typed state. Typed state → encode(). encode() → Canonical query.

The same model decodes a query string into typed values (with issues for invalid input) and encodes typed values back into a canonical query string.
  • 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=b carries meaning.
  • One update() means one encode, one navigation write, and one subscriber notification. Calling runtime.update() or runtime.transaction() does not encode the URL twice, create duplicate history entries, or fire your listener multiple times for that single call. A transaction() 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=newsletter and your model only manages page, updating page writes ?page=3&utm_source=newsletter. You do not have to model every query parameter before you can manage some of them.
  • 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.