npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chronicstone/vue-route-query

v1.0.3

Published

Type-safe URL query parameter synchronization for Vue 3 with Zod validation

Readme

@chronicstone/vue-route-query

npm version npm downloads bundle size license TypeScript

A powerful Vue 3 composable for type-safe URL query parameter synchronization with Zod validation, automatic state management, and intelligent default handling.

Features

  • 🔒 Type-safe: Full TypeScript support with Zod schema validation
  • 🔄 Bidirectional sync: Automatic synchronization between component state and URL
  • 🎯 Deep object support: Handles nested objects and arrays with proper serialization
  • Performance optimized: Batched updates with async processing
  • 🧹 Smart cleanup: Automatically removes default values from URL
  • 🔌 Vue Router integration: Seamless integration with Vue Router
  • 🎨 Flexible API: Support for single values or complex objects
  • 🔗 Nested path support: Deep object structures automatically transformed to dot notation
  • 🔄 Instance sync: Multiple instances with the same key stay synchronized
  • 📜 History control: Choose between push and replace modes for router navigation

Table of Contents

Installation

# npm
npm install @chronicstone/vue-route-query zod vue-router

# yarn
yarn add @chronicstone/vue-route-query zod vue-router

# pnpm
pnpm add @chronicstone/vue-route-query zod vue-router

# bun
bun add @chronicstone/vue-route-query zod vue-router

Basic Usage

Single Value

import { useRouteQuery } from '@chronicstone/vue-route-query'
import { z } from 'zod'

// Single value with key
const activeLayout = useRouteQuery({
  key: 'layout',
  schema: z.enum(['table', 'grid']),
  default: 'table'  // Won't appear in URL when value is 'table'
})

// Type: Ref<'table' | 'grid'>

Object Schema

const filters = useRouteQuery({
  schema: {
    search: z.string(),
    status: z.array(z.string()),
    date: z.object({
      from: z.string(),
      to: z.string()
    })
  },
  default: {
    search: '',
    status: [],
    date: { from: '', to: '' }
  }
})

// Type: Ref<{ search: string; status: string[]; date: { from: string; to: string } }>

Object Schema with Root Key

const userSettings = useRouteQuery({
  key: 'settings',  // Optional for object schemas - adds root prefix to all properties
  schema: {
    theme: z.string(),
    notifications: z.boolean()
  },
  default: {
    theme: 'light',
    notifications: true
  }
})

// URL: ?settings.theme=dark&settings.notifications=false
// Without key: ?theme=dark&notifications=false

Nullable Schema

const sort = useRouteQuery({
  schema: {
    key: z.string(),
    dir: z.enum(['asc', 'desc'])
  },
  default: { key: 'id', dir: 'asc' },
  nullable: true  // Allows the entire object to be null
})

// Type: Ref<{ key: string; dir: 'asc' | 'desc' } | null>

API Reference

useRouteQuery<Schema, Nullable, Output>(config)

The main composable for managing URL query parameters.

Parameters

| Parameter | Type | Required | Description | | ---------- | ---------------------------------------- | -------------------------- | ----------------------------------------------------------------------- | | schema | z.ZodType \| Record<string, z.ZodType> | Yes | Zod schema for validation | | default | NonNullable<Output> | Yes | Default value (won't appear in URL when active) | | key | string | Required for single values | Root key for single value schemas or optional prefix for object schemas | | nullable | boolean | No | Whether the entire value can be null | | enabled | boolean | No | Enable/disable URL synchronization | | debug | boolean | No | Enable debug logging | | mode | 'push' \| 'replace' | No | Navigation mode (default: 'replace') |

Returns

Ref<Output> - A reactive reference to the synchronized state

Important Behavior Notes

  1. Default Values: Default values are never shown in the URL. A parameter only appears in the URL when its value differs from the default.

  2. Root Keys for Object Schemas: When using a key with object schemas, it acts as a prefix for all properties:

    // With key
    useRouteQuery({ key: 'user', schema: { name: z.string() }, default: { name: '' } })
    // URL: ?user.name=John
       
    // Without key
    useRouteQuery({ schema: { name: z.string() }, default: { name: '' } })
    // URL: ?name=John
  3. Nested Objects: Deep object structures are automatically flattened using dot notation:

    // State
    { filters: { date: { from: '2024-01-01' } } }
       
    // URL
    ?filters.date.from=2024-01-01
  4. Arrays: Arrays are JSON stringified in the URL:

    // State
    { tags: ['vue', 'typescript'] }
       
    // URL
    ?tags=["vue","typescript"]
  5. Multiple Instances: Multiple useRouteQuery instances with the same key will stay synchronized. However, ensure they use compatible schemas to avoid conflicts.

  6. Schema Validation: Don't use Zod's .default() function - use the default parameter instead.

  7. Navigation Mode: The mode parameter controls how router navigation occurs:

    • 'replace' (default): Updates the URL without creating a new history entry
    • 'push': Creates a new history entry for each update

    When multiple instances update simultaneously, if any instance uses 'push', the router will use push mode for that batch of updates.

Advanced Usage

Navigation Mode Control

// Using push mode for filters to enable browser back/forward navigation
const filters = useRouteQuery({
  schema: {
    category: z.string(),
    priceRange: z.object({
      min: z.number(),
      max: z.number()
    })
  },
  default: {
    category: '',
    priceRange: { min: 0, max: 1000 }
  },
  mode: 'push'  // Each filter change creates a history entry
})

// Using replace mode for preferences (default)
const preferences = useRouteQuery({
  schema: {
    view: z.enum(['list', 'grid']),
    density: z.enum(['compact', 'comfortable'])
  },
  default: {
    view: 'list',
    density: 'comfortable'
  }
  // mode: 'replace' is the default
})

// If both update simultaneously and filters uses 'push',
// the router will use push for that update

Object Schema with Root Key Prefix

const accountSettings = useRouteQuery({
  key: 'account',  // All properties will be prefixed with 'account.'
  schema: {
    profile: z.object({
      name: z.string(),
      email: z.string()
    }),
    preferences: z.object({
      theme: z.enum(['light', 'dark']),
      notifications: z.boolean()
    })
  },
  default: {
    profile: { name: '', email: '' },
    preferences: { theme: 'light', notifications: true }
  }
})

// URL structure:
// ?account.profile.name=John&[email protected]&account.preferences.theme=dark
// Without the key, it would be:
// ?profile.name=John&[email protected]&preferences.theme=dark

Complex Filtering System

const filters = useRouteQuery({
  schema: {
    searchQuery: z.string().optional(),
    filters: z.object({
      statuses: z.array(z.string()),
      categories: z.array(z.string()),
      authorizationLabels: z.boolean(),
      startDate: z.object({
        from: z.string(),
        to: z.string()
      })
    }),
    quickFilters: z.record(z.string(), z.any())
  },
  default: {
    searchQuery: '',
    filters: {
      statuses: [],
      categories: [],
      authorizationLabels: false,
      startDate: { from: '', to: '' }
    },
    quickFilters: {}
  }
})

// URL when changed from default:
// ?searchQuery=test&filters.statuses=["TO_CHECK_EP","VALIDATED"]&filters.categories=["19KZisAzakz3WESKnUy_C"]&filters.authorizationLabels=true&filters.startDate.from=2025-04-23&filters.startDate.to=2025-04-24

Sortable Table with Nullable State

const sort = useRouteQuery({
  schema: {
    key: z.string(),
    dir: z.enum(['asc', 'desc'])
  },
  default: { key: 'createdAt', dir: 'desc' },
  nullable: true
})

// Can be set to null to disable sorting
sort.value = null

// URL when null: parameters removed
// URL when default: parameters removed
// URL when custom: ?key=name&dir=asc

Dynamic Schema with Persistence Control

const userPreferences = useRouteQuery({
  schema: {
    theme: z.enum(['light', 'dark', 'system']),
    density: z.enum(['compact', 'comfortable', 'spacious']),
    notifications: z.object({
      email: z.boolean(),
      push: z.boolean(),
      frequency: z.enum(['instant', 'daily', 'weekly'])
    })
  },
  default: {
    theme: 'system',
    density: 'comfortable',
    notifications: {
      email: true,
      push: false,
      frequency: 'daily'
    }
  },
  enabled: shouldPersistPreferences.value // Conditionally enable URL sync
})

Pagination with Type Safety

const pagination = useRouteQuery({
  schema: {
    pageSize: z.number(),
    pageIndex: z.number()
  },
  default: {
    pageSize: 20,
    pageIndex: 1
  },
  mode: 'push'  // Enable history for pagination
})

// Only appears in URL when different from default
// ?pageSize=50&pageIndex=3

Under the Hood

State Management Lifecycle

  1. Initialization: The composable initializes with either URL values (if present) or default values
  2. Synchronization: Changes to the ref automatically update the URL, and URL changes update the ref
  3. Cleanup: When values match defaults, they're removed from the URL
  4. Batching: Multiple rapid updates are batched and processed in the next tick

URL Transformation Rules

  1. Objects: Nested objects use dot notation

    { user: { settings: { theme: 'dark' } } }
    // Becomes: ?user.settings.theme=dark
  2. Arrays: Arrays are JSON stringified

    { tags: ['vue', 'ts'] }
    // Becomes: ?tags=["vue","ts"]
  3. Booleans: Represented as string values

    { active: true }
    // Becomes: ?active=true
  4. Numbers: Preserved as numeric strings

    { count: 42 }
    // Becomes: ?count=42
  5. Null/Undefined: Removed from URL entirely

Global Query Manager

The library uses a singleton GlobalQueryManager that:

  • Batches multiple updates to prevent race conditions
  • Processes all updates in the next tick
  • Ensures consistent state across all instances
  • Handles cleanup of removed properties
  • Intelligently combines navigation modes (push if any instance requests push)

Performance Considerations

  1. Batching: All updates are batched to minimize router operations
  2. Shallow Comparison: Uses shallow comparison for primitives
  3. Deep Comparison: Uses recursive comparison for objects only when needed
  4. URL Size: Be mindful of browser URL length limits with large data structures

Browser Support

Works in all modern browsers that support:

  • Vue 3
  • URLSearchParams API
  • ES2015+

TypeScript Support

The library is written in TypeScript and provides full type inference:

// Inferred type based on schema
const data = useRouteQuery({
  schema: {
    name: z.string(),
    age: z.number().optional()
  },
  default: { name: '', age: undefined }
})

// data is Ref<{ name: string; age?: number }>

Common Patterns

Resetting to Defaults

const filters = useRouteQuery({...})

// Reset to default (removes from URL)
filters.value = { ...defaultFilters }

Conditional Parameters

const config = useRouteQuery({
  schema: {
    advanced: z.boolean(),
    // Only used when advanced is true
    customSettings: z.object({...}).optional()
  },
  default: {
    advanced: false,
    customSettings: undefined
  }
})

Synchronized Instances

// Both instances stay in sync
const userSettings1 = useRouteQuery({
  key: 'settings',
  schema: z.object({...}),
  default: {...}
})

const userSettings2 = useRouteQuery({
  key: 'settings',  // Same key
  schema: z.object({...}),  // Must be compatible
  default: {...}
})

Troubleshooting

Common Issues

  1. Schema Mismatch: Ensure multiple instances with the same key use compatible schemas
  2. Default Values: Remember that default values never appear in the URL
  3. Type Errors: Use proper TypeScript types when working with refs
  4. Performance: For large data structures, consider pagination or filtering
  5. Navigation Conflicts: When using mixed modes, push takes precedence over replace

Debug Mode

Enable debug mode to see internal operations:

const data = useRouteQuery({
  // ... other options
  debug: true
})

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a PR.