A Nuxt module that brings Zod into your app with auto-imported composables, a $zod plugin, and first-class server-side support via Nitro.
useZod() composable — available in components, pages, and Nitro server routesuseZodSchemas() — auto-discovers shared Zod registries from shared/schemas/ (flat and nested) with full TypeScript inference$zod plugin instance accessible anywhere via useNuxtApp()useZod() auto-import in Nitro and explicit #nuxt-zod/server aliasevent.validate() on H3Event — validate body, query, and params with typed results and configurable 422 errorsapp.config.ts (zod.errors) for both Nuxt app and NitroNuxtApp and Vue component instanceszod/v3 (and zod/v4 + zod/v4/core when zodVersion: 'v4', or zod/mini + zod/v4/core when zodVersion: 'mini') — not the bare zod root — for faster HMR and to avoid pulling huge optional trees (e.g. all locales) into the client graph on some Zod releasesnuxt-zod gives you a Nuxt-native Zod workflow with zero boilerplate.
useZod() — no manual imports needed$zod plugin available globally across the appuseZod() auto-imported too#nuxt-zod/server alias for static analysis and tree-shakingconst 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
Install the module in your Nuxt project:
npx nuxi@latest module add nuxt-zod
Zod is now available globally in your app. ✨
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 pluginThe $zod instance is also available via useNuxtApp():
const { $zod } = useNuxtApp()
const schema = $zod.object({ name: $zod.string() })
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 }
})
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.
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).
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.date for ISO date strings.errors.<type>.<rule> — e.g. errors.string.min under a nested string object.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.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 }
})
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.
#nuxt-zod/serverFor 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 }
})
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, default true) — Enables the $zod plugin and useZod() auto-import in the Nuxt app (client + SSR).server (boolean, default true) — Enables useZod() in Nitro, the #nuxt-zod/server alias, and event.validate().schemas (object) — Auto-discovery for useZodSchemas(). Set enabled: false to disable. dir is relative to the Nuxt project root. When client or server is false, useZodSchemas() is only registered for the side that remains enabled.zodVersion ('v3' | 'v4' | 'mini') — Which Zod API useZod(), $zod, and #nuxt-zod/server expose. See zodVersion — v3 vs v4 vs mini below.validation (object) — Defaults for event.validate() HTTP errors when validation fails (see next list).src/runtime/v3/, src/runtime/v4/, and src/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 from nuxtZod.zodVersion. Shared: src/runtime/zod-compat.ts. Public validation types (v3+v4 union) live in src/runtime/v4/validation-types.ts; H3Event.validate is augmented in the generated types/nuxt-zod.d.ts from the module. With zodVersion: 'v3', run nuxi analyze on the playground and confirm zod/v4 does not appear in app/server chunks that should be v3-only.^3.25.0 or ^4.0.0 (the module’s peer range). zodVersion: 'mini' requires Zod 4 (zod/mini). Releases below 3.25 often appear in nuxi analyze as one large zod/.../lib/index.mjs in _nitro.mjs because subpath builds are coarser. With zodVersion: 'v4' or 'mini', Nitro still includes zod/v3 on purpose (dual schemas for event.validate() and zod/v3 error-map parity). In server routes, prefer import from zod/v3, zod/v4, or zod/mini, or #nuxt-zod/server / useZod(), instead of from 'zod', to avoid pulling the package root when you only need one surface.v3 — Exposes zod/v3 as z. event.validate() accepts Zod 3 schemas only; the server bundle stays free of zod/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) as z. event.validate() accepts both Zod 3 and Zod 4 schemas in the same call (dispatch uses Zod 4’s _zod marker on instances). Nitro still ships zod/v3 for dual-parse and global error-map parity with zod/v3 imports.mini — Exposes Zod Mini (zod/mini) as z (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 call z.config(z.locales.en()) (or another locale) yourself, or override via app.config → zod.errors. The #nuxt-zod/server virtual re-exports the Mini namespace as z (import * as z from 'zod/mini').zodVersion is omitted from nuxt.config, the module defaults to 'v4' and logs a warning asking you to set zodVersion: 'v4' explicitly. Set zodVersion to 'v3', 'v4', or 'mini' to silence it.nuxtZod.validation
statusCode (number, default 422) — HTTP status when validation fails.message (string, default 'Validation failed') — statusMessage on the thrown error.includeIssues (boolean, default true) — When true, the error payload includes Zod issues grouped by body / query / params.You can import and reuse these types in your own server helpers:
import type { ValidationSchema, ValidationOptions, InferValidated } from 'nuxt-zod'
Property 'validate' does not exist on type 'H3Event'npm run dev:prepare to regenerate Nuxt/Nitro generated types.playground/server/*, restart nuxt dev playground after type generation.server: true in nuxtZod options./ returns page not found in playgroundplayground/app.vue as shell (<NuxtPage />).playground/pages/index.vue and additional routes in playground/pages/*.import { z } from 'zod' everywhere. With: useZod() everywhere.$zod / plugin — Without: wire your own plugin. With: $zod on useNuxtApp().zod in every handler. With: useZod() auto-imported in Nitro.NuxtApp augmentation. With: generated types for $zod and #nuxt-zod/server.server/api/*)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.
MIT — Made with ❤️ by Darlan Prado