Schema Validations
Schema validation using Zod ensures type-safe request handling with automatic parsing and error handling.
Overview
@aura-stack/router integrates seamlessly with Zod to validate:
- Request bodies
- Query parameters (search params)
When validation fails, an error is thrown automatically.
Query Parameter Validation
Basic Query Validation
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"
import { z } from "zod"
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 (request, ctx) => {
const { q, page } = ctx.searchParams
return Response.json({ query: q, page, results: [] })
},
searchConfig
)Advanced Query Validation
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 Body Validation
const createUserConfig = createEndpointConfig({
schemas: {
body: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
},
})
const createUser = createEndpoint(
"POST",
"/users",
async (request, ctx) => {
const { name, email } = ctx.body
return Response.json({ id: "new-id", name, email }, { status: 201 })
},
createUserConfig
)Complex Body Validation
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 (request, ctx) => {
const { profile, settings, tags } = ctx.body
return Response.json({ updated: true })
},
updateProfileConfig
)Combined Validation
Validate both body and query parameters in the same endpoint:
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 (request, 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
)Validation with Route Parameters
Combine route params with validated schemas:
const updateUserConfig = createEndpointConfig("/users/:userId", {
schemas: {
body: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
},
middlewares: [
async (request, ctx) => {
const { userId } = ctx.params
console.log(`Updating user ${userId}`)
return ctx
},
],
})
const updateUser = createEndpoint(
"PATCH",
"/users/:userId",
async (request, ctx) => {
const { userId } = ctx.params
const updates = ctx.body // Typed based on schema
return Response.json({ id: userId, ...updates })
},
updateUserConfig
)Error Handling
Validation errors are automatically thrown with descriptive messages:
Query Parameter Errors
// Request: GET /search?page=invalid
// Error: Invalid search parameters: Expected number, received stringBody Validation Errors
// Request: POST /users with { email: "invalid" }
// Error: Invalid request bodyCustom Error Handling
const { POST } = createRouter([createUser])
async function handleRequest(request: Request) {
try {
return await POST(request, {})
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("Invalid")) {
return Response.json(
{
error: "Validation failed",
message: error.message,
},
{ status: 400 }
)
}
}
return Response.json({ error: "Internal Server Error" }, { status: 500 })
}
}Best Practices
1. Use Descriptive Schemas
// ❌ Too loose
z.string()
// ✅ Specific validation
z.string().email().max(255)2. Provide Default Values
const config = createEndpointConfig({
schemas: {
searchParams: z.object({
page: z.coerce.number().default(1),
limit: z.coerce.number().default(20),
sort: z.enum(["asc", "desc"]).default("asc"),
}),
},
})3. Use Coercion for Query Params
Query parameters are always strings, use coercion:
z.object({
page: z.coerce.number(), // Converts "10" to 10
active: z.coerce.boolean(), // Converts "true" to true
})4. Reuse Schemas
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const createConfig = createEndpointConfig({
schemas: { body: userSchema },
})
const updateConfig = createEndpointConfig({
schemas: { body: userSchema.partial() },
})