Capítulo 12 de 24

Chapter 12: Search Param Validator Adapters

Core Idea

Shows the same feature (a search text query param, defaulted to '', driving a users list) implemented three times with three different schema libraries: zod (via @tanstack/zod-adapter), valibot (native, via @tanstack/valibot-adapter dependency), and arktype (via @tanstack/arktype-adapter). It proves validateSearch is library-agnostic as long as an adapter (or the library's own TanStack-compatible schema) is used.

Setup

  • Routing style: file-based, three parallel routes: /users/zod/, /users/valibot/, /users/arktype/
  • Key dependencies: @tanstack/react-router, @tanstack/router-plugin, @tanstack/react-query, @tanstack/zod-adapter, @tanstack/valibot-adapter, @tanstack/arktype-adapter, zod, valibot, arktype
  • Structure: src/routes/users/zod.index.tsx, valibot.index.tsx, arktype.index.tsx each define validateSearch, loaderDeps, and a loader that primes a shared usersQueryOptions(search) query; shared UI lives in src/components/{Header,Users,Content,Search}.

Code Example

// src/routes/users/zod.index.tsx
import { fallback, zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

const fallbackString = fallback as unknown as (
  schema: z.ZodString,
  fallback: string,
) => z.ZodType<string, z.ZodTypeDef, string>

export const Route = createFileRoute('/users/zod/')({
  validateSearch: zodValidator(
    z.object({
      search: fallbackString(z.string(), '').default(''),
    }),
  ),
  loaderDeps: (opt) => ({ search: opt.search }),
  loader: (opt) => {
    opt.context.queryClient.ensureQueryData(
      usersQueryOptions(opt.deps.search.search ?? ''),
    )
  },
  component: Zod,
})
  • What it demonstrates: zodValidator() wraps a zod schema so validateSearch can consume it directly, and fallback() provides the same catch/default behavior as zod's own .catch(), wired through the adapter.

Key Takeaways

  1. All three routes converge on the identical loader shape (loaderDeps selecting search, then ensureQueryData(usersQueryOptions(search))), the validator library only changes how validateSearch is written, not how the rest of the route works.
  2. Valibot and arktype schemas can be passed to validateSearch more directly (v.object({...}), type({...})) since their output shape already matches what TanStack Router expects, no zodValidator-style wrapper call is needed for them here.
  3. Each library expresses "default to empty string on missing/invalid input" differently: zod uses fallback(...).default(''), valibot uses v.fallback(v.optional(v.string(), ''), ''), arktype uses the inline 'string = ""' syntax, useful reference when migrating between libraries.
  4. Pick the adapter package matching whichever schema library the rest of the codebase already standardizes on, mixing validators per-route (as this example does deliberately, for comparison) is not a typical production pattern.

Connects To

  • ch011-basic-default-search-params: Same underlying concern (typed, defaulted search params) shown with plain zod and no adapter package; read together to see the adapter's value-add over raw zod usage.