Vue cookbook · Intermediate
Validate input in Nuxt server routes
API routes trust request bodies and query strings, returning 500s on bad input.
Approach
Validate bodies, params and query strings with zod through h3's validated readers, return 400 with field errors
via createError, and share schemas with the front end.
Solution
// server/api/orders/index.post.ts
import { z } from "zod";
const NewOrder = z.object({
productId: z.string().min(1),
quantity: z.number().int().min(1).max(10),
note: z.string().max(200).optional(),
});
export default defineEventHandler(async (event) => {
const result = await readValidatedBody(event, (body) => NewOrder.safeParse(body));
if (!result.success) {
throw createError({ statusCode: 400, statusMessage: "Invalid order", data: z.flattenError(result.error).fieldErrors });
}
const user = await requireUser(event); // your auth helper in server/utils
const order = await createOrder(user.id, result.data); // your data access in server/utils
setResponseStatus(event, 201);
return order;
});
declare function requireUser(event: unknown): Promise<{ id: string }>;
declare function createOrder(userId: string, input: z.infer<typeof NewOrder>): Promise<{ id: string }>;
// server/api/orders/index.get.ts
import { z } from "zod";
const Query = z.object({
page: z.coerce.number().int().min(1).default(1),
status: z.enum(["open", "shipped"]).optional(),
});
export default defineEventHandler(async (event) => {
const query = await getValidatedQuery(event, (q) => Query.parse(q)); // throws 400 on invalid input
return { page: query.page, status: query.status ?? "all", items: [] };
});
Pitfalls
- Returning ZodError messages verbatim to users; map them to friendly field errors.
- Validating only on the client.
- Forgetting authorization checks after validation.
Variations
- Put schemas in
shared/so pages and server routes import the same definitions. nuxt-zodor valibot offer alternative validation approaches.
Further reading: Nuxt server directory
Reference: https://nuxt.com/docs/4.x/guide/directory-structure/server
More Vue recipes
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch