Nuxt Zod
A Nuxt module that brings Zod into your app with auto-imported composables, a $zod plugin, and first-class server-side support via Nitro.
Features
- 🔌 Auto-imported
useZod()composable — available in components, pages, and Nitro server routes - 📁
useZodSchemas()— auto-discovers shared Zod registries fromshared/schemas/(flat and nested) with full TypeScript inference - 🛠
$zodplugin instance accessible anywhere viauseNuxtApp() - 🌐 Server-side support with
useZod()auto-import in Nitro and explicit#nuxt-zod/serveralias - ✅
event.validate()onH3Event— validatebody,query, andparamswith typed results and configurable422errors - 🌍 Global Zod issue messages via
app.config.ts(zod.errors) for both Nuxt app and Nitro - 🏷️ Full TypeScript augmentation for
NuxtAppand Vue component instances - ⚡ Vite pre-bundles
zod/v3(andzod/v4+zod/v4/corewhenzodVersion: 'v4', orzod/mini+zod/v4/corewhenzodVersion: 'mini') — not the barezodroot — for faster HMR and to avoid pulling huge optional trees (e.g. all locales) into the client graph on some Zod releases - 📦 Compatible with Zod v3, v4 Classic, and Zod Mini
Why use nuxt-zod?
nuxt-zod gives you a Nuxt-native Zod workflow with zero boilerplate.
- Auto-imported
useZod()— no manual imports needed $zodplugin available globally across the app- Nitro server routes get
useZod()auto-imported too - Explicit
#nuxt-zod/serveralias for static analysis and tree-shaking - Full TypeScript support out of the box
Quick Example
const z = useZod()
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18),
})
const result = userSchema.safeParse({
name: 'Ada Lovelace',
email: 'ada@example.com',
age: 36,
})
console.log(result.success) // true
Quick Setup
Install the module in your Nuxt project:
npx nuxi@latest module add nuxt-zod
Zod is now available globally in your app. ✨
Usage
Client-side with useZod()
Access the Zod z namespace anywhere in your app via the auto-imported useZod() composable:
<template>
<div>
<input v-model="email" placeholder="Email" />
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
const z = useZod()
const email = ref('')
const error = ref('')
const schema = z.string().email('Invalid email address')
watch(email, (value) => {
const result = schema.safeParse(value)
error.value = result.success ? '' : result.error.issues[0].message
})
</script>
$zod plugin
The $zod instance is also available via useNuxtApp():
const { $zod } = useNuxtApp()
const schema = $zod.object({ name: $zod.string() })
Server-side with useZod() (Nitro auto-import)
useZod() is auto-imported in all Nitro server routes, middleware, and utils:
// server/api/validate.ts
export default defineEventHandler(async (event) => {
const z = useZod()
const body = await readBody(event)
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const result = schema.safeParse(body)
if (!result.success) {
throw createError({ statusCode: 422, data: result.error.issues })
}
return { success: true, data: result.data }
})
Nitro event.validate() (body, query, params)
H3Event is extended with event.validate() to parse the request once and return only the fields you list. Schemas can be combined in any way (body, query, params, or any combination). On failure, the response body (Nuxt / h3) includes your payload under data, with Zod issues when includeIssues is true.
// server/api/example.post.ts
export default defineEventHandler(async (event) => {
const z = useZod()
const { body, query } = await event.validate({
body: z.object({ name: z.string() }),
query: z.object({ page: z.coerce.number().optional() }),
})
return { ok: true, body, query }
})
event.validate() uses async-safe parsing, so async Zod refinements/transforms are supported.
Default error behavior is configured under nuxtZod.validation (see below). You can override it per call: await event.validate(schemas, { statusCode, message, includeIssues }).
Types for your own helpers: ValidationSchema, ValidationOptions, and InferValidated are exported from the nuxt-zod package and re-exported for types from #nuxt-zod/server.
Shared registry with useZodSchemas()
Place Zod registry objects (one default export per file) under shared/schemas/. The file path becomes the key path: shared/schemas/user.ts → useZodSchemas().user, and shared/schemas/auth/login.ts → useZodSchemas().auth.login. Each file must export default an object whose values are Zod schemas (or nested groups you choose to expose). Files named index.ts are ignored. Path segments with hyphens or underscores are normalized to camelCase for the property name (e.g. my-user.ts → myUser).
In schema files, prefer import { z } from 'zod' so the same code works in every environment. It is equivalent to const z = useZod() in app or server code, but shared/schemas is not always processed by the same auto-import rules as composables/, so an explicit zod import is the most reliable option.
Client or shared UI code
const { user, auth } = useZodSchemas()
const result = user.create.safeParse(formData)
Nitro with event.validate()
export default defineEventHandler(async (event) => {
const { user } = useZodSchemas()
const { body } = await event.validate({ body: user.create })
return body
})
In nuxt dev, adding, renaming, or removing files under the configured schemas directory triggers a rebuild of the generated registry (no full manual restart required in normal cases).
Global Zod messages (app.config.ts)
Set global Zod issue messages in app.config.ts under zod.errors. You can use a string per type, nested rules per type, ISO helpers, legacy keys by Zod issue code, or default.
// app.config.ts
export default defineAppConfig({
zod: {
errors: {
string: {
invalid_type: 'Not a string',
min: 'Too short',
},
number: {
invalid_type: 'Not a number',
min: 'Number too small',
},
iso: {
date: 'Invalid ISO date',
},
default: 'Invalid value',
},
},
})
This applies in both the Nuxt app runtime and Nitro. Schema-level messages, per-parse options, and code that runs after nuxt-zod still win over these globals.
Compatibility note for library authors: nuxt-zod keeps its public API on the root zod export (useZod(), $zod, and #nuxt-zod/server) so consumer code behaves as expected, while internal issue normalization follows a v3/v4 compatibility layer strategy aligned with Zod library author guidance.
Message resolution order (first match wins; if nothing matches, Zod’s built-in message is used):
errors.iso.<rule>— e.g.errors.iso.datefor ISO date strings.errors.<type>.<rule>— e.g.errors.string.minunder a nestedstringobject.errors.<type>— a single string applies as the default for that type (e.g.string: 'Not a string').errors.<issueCode>— fallback by Zod issue code (e.g.invalid_type).errors.default— catch-all before Zod’s default.
Local override example
export default defineEventHandler(async (event) => {
const z = useZod()
const { body } = await event.validate(
{ body: z.object({ name: z.string().min(1) }) },
{ includeIssues: false, message: 'Bad input' },
)
return { ok: true, body }
})
Error payload shape (Nuxt / h3)
event.validate() throws createError(...). In Nuxt error responses, your custom payload is nested under data:
{
"statusCode": 422,
"statusMessage": "Validation failed",
"data": {
"validation": true,
"issues": {
"body": [
{ "code": "invalid_type", "message": "..." }
]
}
}
}
If includeIssues is false, issues is omitted.
Explicit server import via #nuxt-zod/server
For static analysis or when you prefer explicit imports in server code:
// server/api/validate.ts
import { z } from '#nuxt-zod/server'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const schema = z.object({ name: z.string() })
const result = schema.safeParse(body)
return { success: result.success }
})
Configuration
Module options (nuxt.config.ts)
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-zod'],
nuxtZod: {
client: true, // Enable useZod() + $zod in app code (default: true)
server: true, // Enable useZod() + #nuxt-zod/server + event.validate() in Nitro (default: true)
schemas: {
enabled: true, // useZodSchemas() + scan shared/schemas (default: true)
dir: 'shared/schemas', // root-relative directory to scan (default: 'shared/schemas')
},
zodVersion: 'v4', // 'v3' | 'v4' | 'mini' — omit to log a startup warning; effective default is 'v4'
validation: {
statusCode: 422,
message: 'Validation failed',
includeIssues: true,
},
},
})
nuxtZod options
client(boolean, defaulttrue) — Enables the$zodplugin anduseZod()auto-import in the Nuxt app (client + SSR).server(boolean, defaulttrue) — EnablesuseZod()in Nitro, the#nuxt-zod/serveralias, andevent.validate().schemas(object) — Auto-discovery foruseZodSchemas(). Setenabled: falseto disable.diris relative to the Nuxt project root. Whenclientorserverisfalse,useZodSchemas()is only registered for the side that remains enabled.zodVersion('v3' | 'v4' | 'mini') — Which Zod APIuseZod(),$zod, and#nuxt-zod/serverexpose. See zodVersion — v3 vs v4 vs mini below.validation(object) — Defaults forevent.validate()HTTP errors when validation fails (see next list).- Contributors — runtime layout: Implementation is split into
src/runtime/v3/,src/runtime/v4/, andsrc/runtime/mini/with the same file names in each tree (plugin.ts,composables/useZod.ts,server/utils/validation.ts,validation-types.ts, …). The module picks one root fromnuxtZod.zodVersion. Shared:src/runtime/zod-compat.ts. Public validation types (v3+v4 union) live insrc/runtime/v4/validation-types.ts;H3Event.validateis augmented in the generatedtypes/nuxt-zod.d.tsfrom the module. WithzodVersion: 'v3', runnuxi analyzeon the playground and confirmzod/v4does not appear in app/server chunks that should be v3-only. - Contributors — Nitro bundle / Zod peer: Use Zod
^3.25.0or^4.0.0(the module’s peer range).zodVersion: 'mini'requires Zod 4 (zod/mini). Releases below 3.25 often appear innuxi analyzeas one largezod/.../lib/index.mjsin_nitro.mjsbecause subpath builds are coarser. WithzodVersion: 'v4'or'mini', Nitro still includeszod/v3on purpose (dual schemas forevent.validate()andzod/v3error-map parity). In server routes, preferimportfromzod/v3,zod/v4, orzod/mini, or#nuxt-zod/server/useZod(), instead offrom 'zod', to avoid pulling the package root when you only need one surface.
zodVersion — v3 vs v4 vs mini
v3— Exposeszod/v3asz.event.validate()accepts Zod 3 schemas only; the server bundle stays free ofzod/v4. Choose this when the whole project is on Zod 3 and you want the smallest Nitro graph.v4(effective default) — Exposes Zod 4 Classic (zod/v4) asz.event.validate()accepts both Zod 3 and Zod 4 schemas in the same call (dispatch uses Zod 4’s_zodmarker on instances). Nitro still shipszod/v3for dual-parse and global error-map parity withzod/v3imports.mini— Exposes Zod Mini (zod/mini) asz(functional, tree-shakable API). Requires Zod 4.event.validate()accepts Zod 3, Zod 4 Classic, and Mini schemas. Mini does not load a default locale — issue messages are"Invalid input"unless you callz.config(z.locales.en())(or another locale) yourself, or override viaapp.config→zod.errors. The#nuxt-zod/servervirtual re-exports the Mini namespace asz(import * as z from 'zod/mini').- Startup warning — If
zodVersionis omitted fromnuxt.config, the module defaults to'v4'and logs a warning asking you to setzodVersion: 'v4'explicitly. SetzodVersionto'v3','v4', or'mini'to silence it.
nuxtZod.validation
statusCode(number, default422) — HTTP status when validation fails.message(string, default'Validation failed') —statusMessageon the thrown error.includeIssues(boolean, defaulttrue) — Whentrue, the error payload includes Zodissuesgrouped bybody/query/params.
Exported types
You can import and reuse these types in your own server helpers:
import type { ValidationSchema, ValidationOptions, InferValidated } from 'nuxt-zod'
Troubleshooting
Property 'validate' does not exist on type 'H3Event'
- Run
npm run dev:prepareto regenerate Nuxt/Nitro generated types. - If the error is in
playground/server/*, restartnuxt dev playgroundafter type generation. - Ensure the module is enabled with
server: trueinnuxtZodoptions.
/ returns page not found in playground
- Keep
playground/app.vueas shell (<NuxtPage />). - Put page content under
playground/pages/index.vueand additional routes inplayground/pages/*.
Comparison
- Auto-import composable — Without: manual
import { z } from 'zod'everywhere. With:useZod()everywhere. $zod/ plugin — Without: wire your own plugin. With:$zodonuseNuxtApp().- Server routes — Without: import
zodin every handler. With:useZod()auto-imported in Nitro. - Types — Without: no
NuxtAppaugmentation. With: generated types for$zodand#nuxt-zod/server.
Works well with
- Nuxt 3 / Nuxt 4 (see Compatibility)
- TypeScript
- vee-validate
- Nuxt server routes (
server/api/*) - Zod v3 and v4 (see Compatibility)
Contributing
Contributions are welcome. Open an issue for bugs or feature ideas, and submit a PR when you're ready.
Normas do projeto para agentes/editores Cursor estão em .cursor/rules/.
For local development and test commands, see package.json.
License
MIT — Made with ❤️ by Darlan Prado