Aura Router

Search Params

Learn how to validate request search params in Aura Stack Router using schema validation.

Search params (query params) can be validated with the schemas.searchParams option.

Defining schemas.searchParams lets the endpoint infer the type of the search params, accessible via ctx.searchParams. If no schema is defined, ctx.searchParams is the native URLSearchParams object instead.

import { z } from "zod"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: z.object({
      q: z.string().min(1),
      page: z.coerce.number().int().positive().default(1),
    }),
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import * as valibot from "valibot"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: valibot.object({
      q: valibot.pipe(valibot.string(), valibot.minLength(1)),
      page: valibot.fallback(
        valibot.pipe(
          valibot.unknown(),
          valibot.transform((input) => Number(input)),
          valibot.number(),
          valibot.integer(),
          valibot.positive()
        ),
        1
      ),
    }),
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import { type } from "arktype"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: type({
      q: "string > 0",
      page: type("string.integer.parse").to("number > 0").default(1),
    }),
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import { Type } from "typebox"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: Type.Object({
      q: Type.String({ minLength: 1 }),
      page: Type.Transform(
        Type.Integer({
          minimum: 1,
          default: 1,
        })
      )
        .Decode((value) => Number(value))
        .Encode((value) => value),
    }),
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)

Before the search params are validated, they can be transformed or normalized with the onSearchParams hook — called after the query string is parsed, but before it's checked against the schema.

import { z } from "zod"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: z.object({
      q: z.string().min(1),
      page: z.coerce.number().int().positive().default(1),
    }),
  },
  hooks: {
    onSearchParams: ({ searchParams }) => {
      searchParams.set("limit", "10")
      const page = searchParams.has("page") ? Number(searchParams.get("page")) : 1
      searchParams.set("offset", String((page - 1) * 10))
    },
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import * as valibot from "valibot"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: valibot.object({
      q: valibot.pipe(valibot.string(), valibot.minLength(1)),
      page: valibot.fallback(
        valibot.pipe(
          valibot.unknown(),
          valibot.transform((input) => Number(input)),
          valibot.number(),
          valibot.integer(),
          valibot.positive()
        ),
        1
      ),
    }),
  },
  hooks: {
    onSearchParams: ({ searchParams }) => {
      searchParams.set("limit", "10")
      const page = searchParams.has("page") ? Number(searchParams.get("page")) : 1
      searchParams.set("offset", String((page - 1) * 10))
    },
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import { type } from "arktype"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: type({
      q: "string > 0",
      page: "(number.integer > 0) = 1",
    }),
  },
  hooks: {
    onSearchParams: ({ searchParams }) => {
      searchParams.set("limit", "10")
      const page = searchParams.has("page") ? Number(searchParams.get("page")) : 1
      searchParams.set("offset", String((page - 1) * 10))
    },
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)
import { Type } from "typebox"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

export const config = createEndpointConfig({
  schemas: {
    searchParams: Type.Object({
      q: Type.String({ minLength: 1 }),
      page: Type.Transform(
        Type.Integer({
          minimum: 1,
          default: 1,
        })
      )
        .Decode((value) => Number(value))
        .Encode((value) => value),
    }),
  },
  hooks: {
    onSearchParams: ({ searchParams }) => {
      searchParams.set("limit", "10")
      const page = searchParams.has("page") ? Number(searchParams.get("page")) : 1
      searchParams.set("offset", String((page - 1) * 10))
    },
  },
})

export const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return ctx.json({ query: q, page, results: [] })
  },
  config
)

Type inference for the search params comes from the schema definition, so you get type-safe access to ctx.searchParams. That inference describes the shape TypeScript expects, not a runtime guarantee — invalid requests are still rejected by the validator itself before the handler runs.