Now in Preview

Typed Queries.
Stable URLs.

query-guard は URL クエリを型安全な状態として扱うための、依存ゼロのコアライブラリです。 Adapter と Resolver を分離し、環境に合わせて柔軟に設計できます。

$ pnpm add @liha-labs/query-guard
GitHub
main.ts
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'

const resolver = {
  resolve: ({ raw }) => ({
    value: {
      page: Number(raw.page ?? 1),
      q: raw.q ?? ''
    }
  }),
  serialize: (value) => ({
    page: String(value.page),
    q: value.q
  })
}

const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { page: 1, q: '' },
  unknownPolicy: 'keep'
})

guard.set({ page: 2 })
/ 01

Introduction

URL クエリを型安全に扱うための、依存ゼロコア + 追加パッケージ構成。

What is query-guard?

query-guard は URL クエリ(search params)をアプリの状態として扱うための TypeScript ライブラリです。URLSearchParams を基盤にしつつ、 Resolver によって型を保証します。

Design goals

  • dependency-freeコアは依存ゼロ。必要に応じて拡張。
  • standardsURLSearchParams など Web 標準に準拠。
  • TS-friendly型推論と明確な API で事故を減らす。
  • adapter-based環境に合わせた Adapter で拡張可能。

Non-goals

  • router-specific特定ルーターへの深い統合は行いません。
  • implicit coercion型変換は Resolver に委譲します。
  • nested params{ a: { b: 1 } } のような深い構造は対象外。

Features

シンプルなコアと、必要に応じて追加できる拡張。

Resolverraw ↔ typed の変換を明示化。
unknownPolicy未知キーの保持/除外を制御。
reset modesclear / write-defaults を選択。
Adapter環境ごとの検索文字列の読み書きを分離。
React hookProvider で共通設定を共有。
Zod resolverzod で検証・型変換を追加。

Compatibility

Environment
Browser (createBrowserAdapter)SSR (custom adapter)
Integration
ReactNext.jsCustom Routers
/ 02

Quick Start

Adapter と Resolver を組み合わせて、URL クエリを型安全な状態に。

STEP 01

Install Core

まずコアパッケージを追加します。

$ pnpm add @liha-labs/query-guard
STEP 02

Minimal Usage

Adapter と Resolver を渡して guard を作成します。

guard.ts
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'

const resolver = {
  resolve: ({ raw }) => ({ value: { q: raw.q ?? '' } }),
  serialize: (value) => ({ q: value.q })
}

const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { q: '' }
})

guard.set({ q: 'hello' })
STEP 03

Typed Resolver

型付き Resolver で raw ↔ typed を明確化します。

resolver.ts
type Queries = {
  page: number
  q: string
}

const resolver = {
  resolve: ({ raw }) => ({
    value: {
      page: Number(raw.page ?? 1),
      q: raw.q ?? ''
    }
  }),
  serialize: (value: Queries) => ({
    page: String(value.page),
    q: value.q
  })
}
STEP 04

unknownPolicy

未知のキーを保持するか、落とすかを選択できます。

policy.ts
const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { page: 1 },
  unknownPolicy: 'drop'
})

// URL上の未知キーを落として保存
guard.set({ page: 2 })
STEP 05

React Integration

React では @liha-labs/query-guard-react を追加し、Provider + Hook で使います。

$ pnpm add @liha-labs/query-guard-react react
react.tsx
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'

const adapter = createBrowserAdapter()
const resolver = {
  resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
  serialize: (value) => ({ page: String(value.page) })
}

function App() {
  return (
    <QueryGuardProvider adapter={adapter} resolver={resolver} defaultValue={{ page: 1 }}>
      <Pager />
    </QueryGuardProvider>
  )
}

function Pager() {
  const { queries, set } = useQueryGuard<{ page: number }>()
  return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}
STEP 06

Zod Resolver

検証や型変換を入れる場合は @liha-labs/query-guard-resolvers を利用します。

$ pnpm add @liha-labs/query-guard-resolvers zod
zod.ts
import { z } from 'zod'
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'
import { zodResolver } from '@liha-labs/query-guard-resolvers'

const schema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  q: z.string().default('')
})

const resolver = zodResolver(schema)

const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { page: 1, q: '' }
})
/ 03

Usage

実務の「困った」を解決する、逆引きリファレンス。

Create a guard

createQueryGuard に Adapter と Resolver を渡して作成します。

guard.ts
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'

const resolver = {
  resolve: ({ raw }) => ({ value: { q: raw.q ?? '' } }),
  serialize: (value) => ({ q: value.q })
}

const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { q: '' }
})

Update & Delete

setundefined は削除として扱われます。

update.ts
// setQueries: 完全な置き換え(serialize結果)
guard.setQueries({ q: 'hello' })

// set: patch(undefinedは削除)
guard.set({ q: undefined }) // URLから削除

Reset

clear は管轄キーを削除、write-defaults は既定値をURLへ書き込みます。

reset.ts
// clear: 管轄キーを削除
guard.reset({ mode: 'clear' })

// write-defaults: defaultValue を書き込み
guard.reset({ mode: 'write-defaults' })

History & unknownPolicy

History 更新方法と未知キーの扱いを制御できます。

policy.ts
const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { page: 1 },
  history: 'push',
  unknownPolicy: 'drop'
})

React Hook

実運用では Provider に共通設定を集約し、画面ごとの差分だけ hook 側で上書きするのが分かりやすいです。

react-provider.tsx
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'

const adapter = createBrowserAdapter()
const resolver = {
  resolve: ({ raw }) => ({
    value: { page: Number(raw.page ?? 1), q: String(raw.q ?? '') }
  }),
  serialize: (value) => ({
    page: String(value.page),
    q: value.q
  })
}

function App() {
  return (
    <QueryGuardProvider
      adapter={adapter}
      resolver={resolver}
      defaultValue={{ page: 1, q: '' }}
      history="replace"
      unknownPolicy="keep"
    >
      <Pager />
    </QueryGuardProvider>
  )
}

function Pager() {
  // Provider から resolver/defaultValue を引き継ぐ
  const { queries, set } = useQueryGuard<{ page: number; q: string }>()
  return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}

React: Hook options で部分上書き

Provider の設定をベースにしつつ、1画面だけ history を変えたい場合の例です。

react-override.tsx
import { useQueryGuard } from '@liha-labs/query-guard-react'

function SearchPage() {
  const { queries, set } = useQueryGuard<{ page: number; q: string }>({
    history: 'push' // Provider の history をこの hook だけ上書き
  })

  const onSubmit = (nextQ: string) => set({ q: nextQ, page: 1 })
  return <button onClick={() => onSubmit('react')}>Search</button>
}

React: options なしで使う場合

何も渡さない場合は fallback が使われます。プロトタイピング向けで、実運用では Resolver の明示を推奨します。

react-minimal.tsx
function Prototype() {
  const { queries, set } = useQueryGuard()
  // fallback resolver: raw 値をそのまま扱う
  // fallback defaultValue: {}
  return <button onClick={() => set({ page: '2' })}>{String(queries.page ?? '1')}</button>
}

Zod Resolver

型変換は Zod 側に任せる構成です(z.coerce を利用)。

zod.ts
import { z } from 'zod'
import { zodResolver } from '@liha-labs/query-guard-resolvers'

const resolver = zodResolver(
  z.object({ page: z.coerce.number().int().min(1).default(1) })
)
/ 04

API Reference

query-guard の主要 API と基本仕様。

createQueryGuard(options)

URL クエリの状態管理を行う QueryGuard を生成します。

createQueryGuard<T>(options: QueryGuardOptions<T>): QueryGuard<T>

createBrowserAdapter()

ブラウザ用 Adapter。popstate を購読し、adapter 経由の setSearch 時にも通知します。

createBrowserAdapter(): QueryGuardAdapter

QueryGuardOptions

adapter
QueryGuardAdapter

検索文字列の読み書きと変更通知を提供します。

resolver
QueryResolver<T>

raw ↔ typed 変換の責務を持ちます。

defaultValue
T

初期値。reset の write-defaults でも利用されます。

unknownPolicy
'keep' | 'drop'

未知キーの保持/除外を制御します。

history
'replace' | 'push'

History API の更新方法を指定します。

QueryGuard API

getSearch()getRaw()getQueries()getMeta()setQueries(next, options?)set(patch, options?)reset(options?)subscribe(listener)
Notes:
  • set()undefined は削除扱い。
  • reset()clear / write-defaults を選択。

QueryResolver / QueryRaw

QueryResolver

Resolver は raw → typed / typed → raw を定義します。

QueryRaw
Record<string, string | string[]>

URLSearchParams から得られる raw データ形式です。

React API

useQueryGuard<T>(options?: UseQueryGuardOptions<T>)
QueryGuardProvider

adapter / resolver / defaultValue / history / unknownPolicyを全 hook に配布します。

Resolution order

hook options → Provider → fallback の順で解決されます。

Fallback

adapter は browser なら createBrowserAdapter()。resolver は passthrough、 defaultValue は {}。defaultValue が無い状態で unknownPolicy: 'drop' が要求された場合は 'keep' にフォールバックします。

non-browser 環境で adapter が未指定の場合、useQueryGuard はエラーを投げます。

/ 05

Examples

最小構成のまま、実務に合わせて拡張していく例。

Provider + Hook

reactprovider

Adapter / policy を Provider で共有し、hook 側は最小オプションに。

react.tsx
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'

const adapter = createBrowserAdapter()
const resolver = {
  resolve: ({ raw }) => ({
    value: { page: Number(raw.page ?? 1), q: String(raw.q ?? '') }
  }),
  serialize: (value) => ({
    page: String(value.page),
    q: value.q
  })
}

function App() {
  return (
    <QueryGuardProvider
      adapter={adapter}
      resolver={resolver}
      defaultValue={{ page: 1, q: '' }}
      history="replace"
      unknownPolicy="keep"
    >
      <Pager />
    </QueryGuardProvider>
  )
}

function Pager() {
  const { queries, set } = useQueryGuard<{ page: number; q: string }>()
  return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}

Hook Override

reactoverride

Provider をベースにしつつ、一部オプションだけ hook 側で上書きできます。

react-override.tsx
import { useQueryGuard } from '@liha-labs/query-guard-react'

function SearchPage() {
  const { queries, set } = useQueryGuard<{ page: number; q: string }>({
    history: 'push'
  })

  const goNext = () => set({ page: queries.page + 1 })
  return <button onClick={goNext}>Next</button>
}

unknownPolicy

dropkeep

URL に混在する未知キーを保持するか、落とすかを選べます。

policy.ts
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'

const resolver = {
  resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
  serialize: (value) => ({ page: String(value.page) })
}

const guard = createQueryGuard({
  adapter: createBrowserAdapter(),
  resolver,
  defaultValue: { page: 1 },
  unknownPolicy: 'drop'
})

// URL から未知キーを除去して保存
guard.set({ page: 2 })

Zod Resolver

zodcoerce

型変換は Zod に任せる構成。z.coerce を使います。

zod.ts
import { z } from 'zod'
import { zodResolver } from '@liha-labs/query-guard-resolvers'

const resolver = zodResolver(
  z.object({ page: z.coerce.number().int().min(1).default(1) })
)

SSR Adapter

ssrcustom

非ブラウザ環境では Adapter を自作して注入します。

ssr-react.tsx
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'

const adapter = {
  getSearch: () => '?page=1',
  setSearch: () => {},
  subscribe: () => () => {}
}

const resolver = {
  resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
  serialize: (value) => ({ page: String(value.page) })
}

function Page() {
  return (
    <QueryGuardProvider adapter={adapter} resolver={resolver} defaultValue={{ page: 1 }}>
      <Inner />
    </QueryGuardProvider>
  )
}

function Inner() {
  const { queries } = useQueryGuard<{ page: number }>()
  return <p>{queries.page}</p>
}
query-guard

query-guard

TypeScript のための URL クエリ状態管理。

検索パラメータを、型安全なアプリ状態へ。

Produced byLiha LabsLiha Labs
© 2026 Liha Labs. Released under the MIT License.