Skip to content
Runtime
Framework

Filters

param.choice gives you a union type and a distinct issue code for an unrecognized value.

const products = defineQueryModel({
status: param.choice(["all", "active", "archived"]).default("all"),
page: param.integer({ min: 1 }).default(1),
});

Making "all" the default is what keeps the URL clean: selecting “All” removes the key rather than writing ?status=all.

// current URL: /products?status=active&page=4
await runtime.transaction((draft) => {
draft.status = "archived";
draft.page = 1;
});
// written URL: /products?status=archived
Switching to Active adds the key; switching back to All removes it, because All is the declared default.

History1 / 1

Products

6 matching

  • Edge runtime handbookactive$59
  • Node.js request toolkitactive$39
  • Nuxt deployment guidearchived$19
  • history.pushState
  • history.replaceState
  • popstate

The adapter writes through the History API and re-reads on popstate.

Type a query, press Enter.

Typed state

{
  "page": 1,
  "sort": "created_at",
  "status": "all"
}

Canonical URL

Valid
/products

    param.list consumes every value stored under its key, so repeated keys become an array.

    const products = defineQueryModel({
    tags: param.list(param.text()).default([]),
    });
    products.decode("?tags=vue&tags=nuxt"); // ok, value: { tags: ["vue", "nuxt"] }
    products.encode({ tags: ["vue", "nuxt"] }); // [["tags", "vue"], ["tags", "nuxt"]]
    products.encode({ tags: [] }); // [] — equals the default; otherwise it would be [["tags", ""]]

    Toggling one value:

    async function toggleTag(tag: string): Promise<void> {
    await runtime.transaction((draft) => {
    draft.tags = draft.tags.includes(tag)
    ? draft.tags.filter((entry) => entry !== tag)
    : [...draft.tags, tag];
    draft.page = 1;
    });
    }

    Assign a new array rather than mutating in place. The draft’s arrays are copies, so mutation would work — but treating them as immutable keeps the intent obvious and survives a refactor to replace.

    const products = defineQueryModel({
    category: param.list(param.choice(["books", "games", "music", "software"]), {
    minItems: 1,
    maxItems: 3,
    }),
    });
    const accepted = products.decode("?category=books&category=music");
    accepted.ok; // true
    const rejected = products.decode("?category=books&category=games&category=music&category=software");
    rejected.issues[0]?.code; // "out_of_range" — four selections exceed maxItems

    Item-level validation runs per entry, and item issues carry their index in path. Count violations report out_of_range against the list itself.

    await runtime.reset(); // every managed key back to its default
    await runtime.reset(["status", "tags"]); // just these
    await runtime.remove(["status", "tags"]); // omit the keys entirely

    For parameters with defaults the three produce the same URL. Prefer reset when the intent is “back to the initial view” — it reads correctly even for parameters you add later.

    const products = defineQueryModel({
    inStock: param.boolean().default(false),
    });
    const result = products.decode("?inStock=yes");
    if (result.ok) result.value.inStock; // true
    products.encode({ inStock: true }); // [["inStock", "true"]]

    Decoding accepts true, 1, yes, on and their negatives, case-insensitively. Encoding writes the first entry of the truthy or falsy list, so the output is always ?inStock=true rather than whichever spelling arrived.

    A valueless flag — ?inStock with no = — is not supported as true. An empty value is rejected before the boolean codec ever sees it: the parameter reports empty and recovers to its default. Custom truthy entries cannot change that, because the emptiness check happens first. Write ?inStock=true, which is what encoding produces anyway.

    Distinguishing “cleared” from “untouched”

    Section titled “Distinguishing “cleared” from “untouched””

    If your application must tell “the user removed this filter” from “the user never set it”, the default cannot express both. Use .nullable():

    const filters = defineQueryModel({
    category: param.text().nullable().optional(),
    });
    const untouched = filters.decode("");
    const cleared = filters.decode("?category=");
    if (untouched.ok) untouched.value.category; // undefined — untouched
    if (cleared.ok) cleared.value.category; // null — explicitly cleared
    const result = readRequestQuery(request, products);
    const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
    const rows = await db.products.findMany({
    where: {
    status: values.status === "all" ? undefined : values.status,
    tags: values.tags.length > 0 ? { hasSome: values.tags } : undefined,
    },
    });

    values.status is a narrow union here, so the comparison is exhaustive and a new status added to the model becomes a compile error in this file.

    • An unknown choice reports unknown_choice and recovers to the default — the page renders with “All” rather than breaking on a stale bookmark.
    • Empty entries in a list (?tags=&tags=vue) are dropped and reported as empty.
    • Order within a list is preserved exactly as it appeared in the query.
    • A list default of [] is compared by encoded form, so it is correctly omitted.