All projects

QueryWeave

Experimental betaSince 2026MIT

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.

QueryWeave landing page: a query string decoded into typed state

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.

ts
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

ts
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

PackageResponsibilitymin+gzip
@queryweave/coreModels, codecs, decode and encode, runtime contracts7.39 kB
@queryweave/browserBrowser History API adapter564 B
@queryweave/serverWeb-standard server helpers469 B
@queryweave/nodeNode.js request integration691 B
@queryweave/standard-schemaStandard Schema interoperability341 B
@queryweave/testingMemory adapter and test utilities491 B
@queryweave/vueVue bindings for query models850 B
@queryweave/vue-routerVue Router adapter699 B
@queryweave/nuxtNuxt module and per-request runtime911 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.

Page
2
Sort

Typed state

{
  search: "vue",
  page: 2,
  sort: "newest",
  archived: false,
}

Canonical URL

/products?search=vue&page=2

Recovered

No issues