Aura Router

Zod Schemas

Schema validations are an optional configuration in createEndpoint and createEndpointConfig. They allow you to apply Zod-based validations to params, searchParams, and body, providing full type-safety for your endpoint's request context.

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

export const endpoint = createEndpoint("POST", "/auth/signIn", async (ctx) => {
  const { username, password } = ctx.body
  return Response.json({ message "Successful Login" })
}, {
  schemas: {
    body: z.object({
      username: z.string(),
      password: z.string()
    })
  }
})

What you'll learn


Good to know

  • For detailed information about schema validations inside createEndpoint, see the API Reference.
  • Zod is not include in @aura-stack/router, so, if you need to add validatons, you need to install zod.
  • Zod schemas are optional but highly recommended to add runtime validation to:
    • params via schemas.params
    • searchParams via schemas.searchParams
    • body via schemas.body

Type Inference

When adding Zod schemas, the endpoint automatically extracts and infers the types from them. These inferred types are applied to:

  • The Route Handler Request Context.
  • The Middleware Context.

This ensures fully typed access to:

  • ctx.params
  • ctx.searchParams
  • ctx.body

The types can be accessed from the main entry points / or /types. Available types:

  • RequestContext.
  • ContextParams.
  • ContextSearchParams.
  • ContextBody.

Installation

To enable schema validation, install Zod:

npm install zod

Schema Validations

Schema validations can be passed directly to an endpoint or using createEndpointConfig

Directly

Pass the schema as the fourth argument of createEndpoint.

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

export const deleteUser = createEndpoint("DELETE", "/users/:userId", async (ctx) => {
  const { userId } = ctx.params
  return Response.json({ message "User DELETE successfully" })
}, {
  schemas: {
    params: z.object({
      userId: z.coerce.number()
    })
  }
})

Using createEndpointConfig

Use createEndpointConfig and pass the config as the fourth argument.

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

const config = createEndpointConfig("/users/:userId", {
  schemas: {
    params: z.object({
      userId: z.coerce.number()
    })
  }
})

export const deleteUser = createEndpoint("DELETE", "/users/:userId", async (ctx) => {
  const { userId } = ctx.params
  return Response.json({ message "User DELETE successfully" })
}, config)

Params

Basic Validation

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

const dynamicsConfig = createEndpointConfig("/users/:userId/books/:bookId", {
  schemas: {
    params: z.object({
      userId: z.regex(/^[0-9]+$/),
      bookId: z.uuid(),
    }),
  },
})

const getBookById = createEndpoint(
  "GET",
  "/users/:userId/books/:bookId",
  async (ctx) => {
    const { userId, bookId } = ctx.params
    return Response.json({ bookId })
  },
  dynamicsConfig
)

Search Params Validation

Basic Validation

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

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

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

Advanced Validation

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

const advancedSearchConfig = createEndpointConfig({
  schemas: {
    searchParams: z.object({
      q: z.string().min(3).max(100),
      category: z.enum(["tech", "science", "arts"]).optional(),
      page: z.coerce.number().int().min(1).default(1),
      limit: z.coerce.number().int().min(1).max(100).default(20),
      sortBy: z.enum(["date", "relevance", "popularity"]).default("relevance"),
      sortOrder: z.enum(["asc", "desc"]).default("desc"),
    }),
  },
})

Request Body Validation

Basic Validation

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

const createUserConfig = createEndpointConfig({
  schemas: {
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }),
  },
})

const createUser = createEndpoint(
  "POST",
  "/users",
  async (ctx) => {
    const { name, email } = ctx.body

    return Response.json({ id: "new-id", name, email }, { status: 201 })
  },
  createUserConfig
)

Advanced Validation

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

const updateProfileConfig = createEndpointConfig({
  schemas: {
    body: z.object({
      profile: z.object({
        firstName: z.string().min(1),
        lastName: z.string().min(1),
        bio: z.string().max(500).optional(),
      }),
      settings: z.object({
        theme: z.enum(["light", "dark"]).default("light"),
        notifications: z.boolean().default(true),
        language: z.string().length(2).default("en"),
      }),
      tags: z.array(z.string()).min(1).max(10),
    }),
  },
})

const updateProfile = createEndpoint(
  "PATCH",
  "/profile",
  async (ctx) => {
    const { profile, settings, tags } = ctx.body
    return Response.json({ updated: true })
  },
  updateProfileConfig
)

Combined Validation

Validate both searchParams and body in the same endpoint:

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

const createPostConfig = createEndpointConfig({
  schemas: {
    body: z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(1),
      tags: z.array(z.string()).optional(),
    }),
    searchParams: z.object({
      publish: z.enum(["true", "false"]).default("false"),
      notify: z.enum(["true", "false"]).default("true"),
    }),
  },
})

const createPost = createEndpoint(
  "POST",
  "/posts",
  async (ctx) => {
    const { title, content, tags } = ctx.body
    const { publish, notify } = ctx.searchParams

    return Response.json(
      {
        id: "new-post",
        title,
        content,
        tags,
        published: publish === "true",
        notified: notify === "true",
      },
      { status: 201 }
    )
  },
  createPostConfig
)