A type-safe, reactive URL search params manager for SvelteKit 2 with Zod 4 validation. Simplify URL state management with automatic synchronization, type inference, and built-in validation.
npm install svelte-url-search-params
pnpm add svelte-url-search-params
yarn add svelte-url-search-params
<script lang="ts">
import { queryParameters, p } from 'svelte-url-search-params';
const params = queryParameters({
search: p.string(''),
page: p.number(1),
enabled: p.boolean(false),
tags: p.array<string>([]),
ids: p.array<number>([])
});
</script>
<input bind:value={params.search} type="text" />
<input bind:value={params.page} type="number" />
<input bind:checked={params.enabled} type="checkbox" />
queryParameters(config, options?)
Creates a reactive query parameters manager.
Parameters:
config
- An object mapping parameter names to Zod schemas (use p.*
helpers)options
- Optional configuration objectReturns: Reactive proxy object with your defined parameters plus utility methods (clear()
, reset()
)
p.*
)p.string(defaultValue: string)
String parameter with trimming.
search: p.string(''); // ?search=hello
p.number(defaultValue?: number)
Number parameter. Returns null
if no default and value is missing.
page: p.number(1); // ?page=42
count: p.number(); // Optional number
p.boolean(defaultValue: boolean = false)
Boolean parameter. Accepts 'true'
, 'false'
, '1'
, '0'
.
enabled: p.boolean(false); // ?enabled=true
p.array<T>(defaultValue: T[] = [])
Array parameter. Automatically detects number vs string arrays and removes duplicates.
tags: p.array<string>([]); // ?tags=a,b,c
ids: p.array<number>([]); // ?ids=1,2,3
categories: p.array<string>(['all']); // With default
p.object<T>(defaultValue: T | null = null)
JSON object parameter. Automatically serializes/deserializes.
filter: p.object<{ min: number; max: number }>(null);
// ?filter=%7B%22min%22%3A0%2C%22max%22%3A100%7D
p.enum<T>(values: readonly T[], defaultValue: T)
Enum parameter that validates against allowed values.
status: p.enum(['draft', 'published', 'archived'], 'draft'); // ?status=published
role: p.enum(['user', 'admin'], 'user'); // ?role=admin
p.integer(defaultValue?: number)
Integer parameter (no decimals). Returns null
if no default and value is missing.
age: p.integer(18); // ?age=25
count: p.integer(); // Optional integer
type QueryParamsOptions = {
/**
* Debounce history updates by this many milliseconds
* @default 0
*/
debounceHistory?: number;
/**
* Whether to push to browser history (false = replaceState)
* @default true
*/
pushHistory?: boolean;
/**
* Whether to sort search params alphabetically
* @default true
*/
sort?: boolean;
/**
* Whether to show default values in the URL
* @default true
*/
showDefaults?: boolean;
/**
* If true, the params will not update after initial load
* Useful for static sites or when URL should not change
* @default false
*/
static?: boolean;
};
Example:
const params = queryParameters(
{
search: p.string(''),
page: p.number(1)
},
{
debounceHistory: 300, // Wait 300ms before updating URL
pushHistory: false, // Use replaceState instead of pushState
sort: true, // Sort params alphabetically
showDefaults: false // Don't show default values in URL
}
);
clear()
Clears all search parameters from the URL.
params.clear();
reset()
Resets all parameters to their default values.
params.reset();
<script lang="ts">
import { queryParameters, p } from 'svelte-url-search-params';
const params = queryParameters({
q: p.string(''),
category: p.string('all'),
minPrice: p.number(),
maxPrice: p.number(),
inStock: p.boolean(false)
});
</script>
<input bind:value={params.q} placeholder="Search..." />
<select bind:value={params.category}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
<input bind:value={params.minPrice} type="number" placeholder="Min" />
<input bind:value={params.maxPrice} type="number" placeholder="Max" />
<label>
<input bind:checked={params.inStock} type="checkbox" />
In Stock Only
</label>
<script lang="ts">
import { queryParameters, p } from 'svelte-url-search-params';
const params = queryParameters({
page: p.number(1),
limit: p.number(10)
});
function nextPage() {
params.page = (params.page ?? 1) + 1;
}
function prevPage() {
params.page = Math.max(1, (params.page ?? 1) - 1);
}
</script>
<button onclick={prevPage} disabled={params.page <= 1}> Previous </button>
<span>Page {params.page}</span>
<button onclick={nextPage}>Next</button>
<script lang="ts">
import { queryParameters, p } from 'svelte-url-search-params';
const params = queryParameters({
tags: p.array<string>([]),
ids: p.array<number>([])
});
const availableTags = ['svelte', 'typescript', 'javascript'];
function toggleTag(tag: string) {
if (params.tags.includes(tag)) {
params.tags = params.tags.filter((t) => t !== tag);
} else {
params.tags = [...params.tags, tag];
}
}
</script>
{#each availableTags as tag}
<label>
<input type="checkbox" checked={params.tags.includes(tag)} onchange={() => toggleTag(tag)} />
{tag}
</label>
{/each}
<script lang="ts">
import { queryParameters, p } from 'svelte-url-search-params';
// Debounce URL updates to avoid excessive history entries
const params = queryParameters(
{
search: p.string('')
},
{
debounceHistory: 500 // Wait 500ms after last change
}
);
</script>
<input bind:value={params.search} type="text" placeholder="Search (debounced)..." />
import { queryParameters, p } from 'svelte-url-search-params';
const params = queryParameters({
status: p.enum(['draft', 'published', 'archived'], 'draft'),
role: p.enum(['user', 'admin'], 'user'),
age: p.integer(18),
rating: p.integer()
});
// Type-safe enums
params.status = 'published'; // â
Works
params.status = 'invalid'; // Will be reset to default 'draft'
// Integers only (no decimals)
params.age = 25; // â
Works
// URL: ?age=25.5 will be parsed as 25
You can create custom parameter types by composing Zod schemas. Remember that URL params are always strings, so you need to transform from string to your desired type:
import { queryParameters } from 'svelte-url-search-params';
import { z } from 'zod';
// Custom email parameter
const pEmail = (defaultValue: string = '') =>
z
.string()
.optional()
.default(defaultValue)
.transform((val) => val.trim())
.refine((val) => !val || z.email().safeParse(val).success, {
message: 'Invalid email'
});
// Custom age parameter with validation
const pAge = (defaultValue: number = 18) =>
z
.string()
.optional()
.default(String(defaultValue))
.transform((val) => {
const num = Number(val);
if (isNaN(num)) return defaultValue;
return Math.min(Math.max(num, 18), 120); // Clamp between 18-120
});
// Custom enum parameter
const pRole = () =>
z
.string()
.optional()
.default('user')
.transform((val) => {
return val === 'admin' ? 'admin' : 'user';
});
const params = queryParameters({
email: pEmail(''),
age: pAge(18),
role: pRole()
});
Or use the built-in helpers with additional validation:
import { queryParameters, p } from 'svelte-url-search-params';
import { z } from 'zod';
const params = queryParameters({
// Simple custom validation on string
username: z
.string()
.optional()
.default('')
.transform((val) => val.toLowerCase().trim()),
// Number with custom range
rating: z
.string()
.optional()
.transform((val) => {
const num = Number(val);
return !isNaN(num) && num >= 1 && num <= 5 ? num : 1;
}),
// Or use p.* helpers as base
tags: p.array<string>([])
});
TypeScript automatically infers types from your config:
const params = queryParameters({
search: p.string(''),
page: p.number(1),
tags: p.array<string>([])
});
// TypeScript knows the types!
params.search; // string
params.page; // number | null
params.tags; // string[]
While queryParameters()
is designed for client-side use, you can use validateQueryParameters()
on the backend to parse and validate URL search params in your SvelteKit server routes (+page.server.ts
, +server.ts
).
// src/routes/products/+page.server.ts
import { validateQueryParameters, p } from 'svelte-url-search-params';
import { z } from 'zod';
import type { PageServerLoad } from './$types';
// Define schema for URL params
const schema = z.object({
category: p.string('all'),
minPrice: p.number(),
maxPrice: p.number(),
page: p.number(1),
tags: p.array<string>([]),
inStock: p.boolean(false)
});
export const load: PageServerLoad = async ({ url }) => {
// Validate params
const { params, errors } = validateQueryParameters(schema, url.searchParams);
// params is fully typed and contains defaults for invalid/missing values
// errors contains any validation errors (but params still has fallback values)
// Fetch data using validated params
const products = await db.products.findMany({
where: {
category: params.category !== 'all' ? params.category : undefined,
price: {
gte: params.minPrice ?? undefined,
lte: params.maxPrice ?? undefined
},
tags: params.tags.length > 0 ? { some: params.tags } : undefined,
inStock: params.inStock || undefined
},
skip: (params.page - 1) * 20,
take: 20
});
return {
products,
filters: params,
validationErrors: errors
};
};
// src/routes/api/search/+server.ts
import { validateQueryParameters, p } from 'svelte-url-search-params';
import { json } from '@sveltejs/kit';
import { z } from 'zod';
import type { RequestHandler } from './$types';
// Define your schema using Zod object
const schema = z.object({
search: p.string(''),
page: p.number(1),
tags: p.array<string>([]),
enabled: p.boolean(false)
});
export const GET: RequestHandler = async ({ url }) => {
// Validate and parse URL search params
const { params, errors } = validateQueryParameters(schema, url.searchParams);
// params is fully typed and contains defaults for invalid/missing values
// errors contains any validation errors (but params still has fallback values)
const results = await searchDatabase({
search: params.search,
page: params.page,
tags: params.tags,
enabled: params.enabled
});
return json({ results, params, errors });
};
validateQueryParameters()
returns { params, errors }
where params
always contains valid values (using defaults for invalid/missing params)errors
array contains any issues, but doesn't throw - you can handle them as neededp.*
helpersp.string()
, p.number()
, p.array()
, p.enum()
, p.integer()
, p.boolean()
, p.object()
params
is fully typed based on your schemaparams.key
parses the URL value using the Zod schemaparams.key = value
updates the URL via goto()
SvelteURLSearchParams
for reactivity with Svelte 5Make sure you're using Svelte 5 and SvelteKit 2. The library relies on Svelte 5's reactivity system.
The library automatically removes duplicates from arrays. If you see duplicates, make sure you're using the latest version.
Ensure your TypeScript version is up to date and that you're using the correct generic types:
// â
Correct
tags: p.array<string>([]);
ids: p.array<number>([]);
// â Wrong - missing type argument
tags: p.array([]);
MIT
Contributions are welcome! Please open an issue or PR on GitHub.