QueryWeave
Typed URL state for the browser, the server and tests.
A URL state engine that does not depend on any framework. You describe the query parameters once as a typed model, and the same model decodes, validates and encodes them in the browser, on the server and in tests. An invalid value falls back to its default and is reported, instead of breaking the page.

The problem
URL state shows up in the browser, on the server, in tests and in shared links. Most codebases describe it more than once: a client parser, a server parser, a serializer, and defaults repeated in each. Those copies drift apart.
One model, synced through adapters
QueryWeave defines query state once, as a model, and syncs it through adapters. Every environment runs the same pipeline: the query string is decoded, validated into typed state, updated, and written back in a canonical encoding.
import { createQueryRuntime, defineQueryModel, param } from '@queryweave/core'
const productFilters = defineQueryModel({
search: param.text().optional(),
page: param.integer({ min: 1 }).default(1),
archived: param.boolean().default(false),
sort: param.choice(['name', 'created_at', 'price']).default('created_at'),
tags: param.list(param.text()).default([]),
})
const runtime = createQueryRuntime({ model: productFilters, adapter })
runtime.read().values.page // 1
await runtime.update({ search: 'vue', page: 1 })
await runtime.update({ page: 2 }, { navigation: 'replace' })
await runtime.transaction((draft) => {
draft.search = 'vue'
draft.page = 1
})
A broken link does not crash the page. ?page=abc decodes to page 1, and the result carries an issue explaining why, so the application can recover and tell the user.
Vue bindings
const filters = useQueryModel(productFilters)
filters.values.page // readonly reactive state
await filters.update({ page: 2 }) // explicit operation
const search = filters.field('search', { navigation: 'replace' }) // v-model target
There is no hook per key, no tuple setter, and no direct mutation as the primary API. Vue and Nuxt are the first frontend integrations, but neither shapes the core.
Nine packages
| Package | Responsibility | min+gzip |
|---|---|---|
@queryweave/core | Models, codecs, decode and encode, runtime contracts | 7.39 kB |
@queryweave/browser | Browser History API adapter | 564 B |
@queryweave/server | Web-standard server helpers | 469 B |
@queryweave/node | Node.js request integration | 691 B |
@queryweave/standard-schema | Standard Schema interoperability | 341 B |
@queryweave/testing | Memory adapter and test utilities | 491 B |
@queryweave/vue | Vue bindings for query models | 850 B |
@queryweave/vue-router | Vue Router adapter | 699 B |
@queryweave/nuxt | Nuxt module and per-request runtime | 911 B |
The budgets are enforced: pnpm bundle:size:check fails when a package grows past its limit.
Testing
The full gate runs every Vitest project, including Vitest Browser Mode and type-level assertions, real-browser tests in Chromium, Firefox and WebKit, and Playwright against the playground. Boundary checks cover sources, manifests and the dependency graph; artifact checks cover the built output and declaration resolution; and the packed archives are installed into clean consumer projects before a release.
The project is experimental and pre-1.0: the public API may change between minor versions, and every breaking change is recorded in an architecture decision record and a changeset.
A closer look
A small URL lab
A small reimplementation written for this page, not the library itself. Change a filter, or load a broken link.
Typed state
{
search: "vue",
page: 2,
sort: "newest",
archived: false,
}Canonical URL
/products?search=vue&page=2
Recovered
No issues
Screens




