23 de June del 2025
Cuando desarrollás APIs en Nuxt, validar los datos que recibís es fundamental para la seguridad y estabilidad de tu aplicación. Gracias a h3 (el framework HTTP que usa Nitro por debajo), tenemos funciones especiales que combinan lectura y validación en un solo paso.
En lugar de usar las funciones tradicionales como readBody
, getQuery
y getRouterParams
, podés usar sus versiones "validated" que no solo obtienen los datos sino que también los validan automáticamente.
Tradicionalmente, cuando querías validar datos en un endpoint, tenías que hacer algo así:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validación manual
if (!body.name || typeof body.name !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'Nombre requerido'
})
}
if (!body.email || !isValidEmail(body.email)) {
throw createError({
statusCode: 400,
statusMessage: 'Email inválido'
})
}
// Más validaciones...
// Finalmente usar los datos
return { message: `Hola ${body.name}` }
})
Esto funciona, pero es repetitivo, propenso a errores y no tenés type safety — TypeScript no sabe qué estructura tienen tus datos validados.
h3 (el framework HTTP que usa Nitro) incluye tres funciones especiales que combinan lectura y validación:
readValidatedBody()
- Para validar el body de requests POST/PUT/PATCHgetValidatedQuery()
- Para validar query parametersgetValidatedRouterParams()
- Para validar parámetros de rutasTodas estas funciones reciben dos parámetros:
throw
o retornar false
si son inválidosSi la validación falla, el endpoint automáticamente devuelve un error HTTP. Si pasa, continuás con los datos ya validados y con el tipo correcto.
Zod es perfecto para esto porque sus funciones parse()
hacen exactamente lo que necesitamos:
import { z } from 'zod'
const bodySchema = z.object({
name: z.string().min(1, 'Nombre requerido'),
email: z.string().email('Email inválido'),
message: z.string().min(10, 'Mensaje muy corto'),
})
export default defineEventHandler(async (event) => {
// body va a tener el tipo { name: string, email: string, message: string }
const body = await readValidatedBody(event, bodySchema.parse)
// Ya podés usar los datos con confianza
return {
message: `Gracias ${body.name}, tu mensaje fue recibido.`
}
})
¡Eso es todo! Con una sola línea tenés:
Si preferís Valibot, podés hacer algo similar envolviendo la función parse
:
import * as v from 'valibot'
const bodySchema = v.object({
name: v.pipe(v.string(), v.minLength(1, 'Nombre requerido')),
email: v.pipe(v.string(), v.email('Email inválido')),
message: v.pipe(v.string(), v.minLength(10, 'Mensaje muy corto')),
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, (data) => v.parse(bodySchema, data))
return {
message: `Hola ${body.name}, recibimos tu consulta.`
}
})
Para validar parámetros de query (como ?page=1&limit=10
), usás getValidatedQuery
:
import { z } from 'zod'
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(10),
search: z.string().optional(),
})
export default defineEventHandler(async (event) => {
const query = getValidatedQuery(event, querySchema.parse)
// query tiene el tipo { page: number, limit: number, search?: string }
return {
pagination: {
page: query.page,
limit: query.limit,
search: query.search
}
}
})
Tip: Usá z.coerce.number()
para convertir automáticamente strings a números, ya que los query params siempre llegan como strings.
Para rutas como /api/users/[id].get.ts
, podés validar el parámetro id
:
import { z } from 'zod'
const paramsSchema = z.object({
id: z.string().uuid('ID debe ser un UUID válido'),
})
export default defineEventHandler(async (event) => {
const params = getValidatedRouterParams(event, paramsSchema.parse)
// params.id es un string que sabemos que es un UUID válido
const user = await getUserById(params.id)
return { user }
})
Acá tenés un ejemplo completo de un endpoint de contacto que valida tanto el body como un token de Turnstile:
import { z } from 'zod'
const contactSchema = z.object({
name: z.string().min(1, 'Nombre es requerido'),
email: z.string().email('Email inválido'),
message: z.string().min(10, 'El mensaje debe tener al menos 10 caracteres'),
'cf-turnstile-response': z.string().min(1, 'Verificación requerida'),
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, contactSchema.parse)
// Verificar Turnstile
const verified = await verifyTurnstileToken(body['cf-turnstile-response'])
if (!verified) {
throw createError({
statusCode: 400,
statusMessage: 'Verificación de Turnstile falló'
})
}
// Enviar email
await sendContactEmail({
name: body.name,
email: body.email,
message: body.message,
})
return { message: 'Mensaje enviado correctamente' }
})
Si necesitás más control sobre los errores, podés envolver tu función validadora:
const body = await readValidatedBody(event, (data) => {
try {
return bodySchema.parse(data)
} catch (error) {
throw createError({
statusCode: 422,
statusMessage: 'Datos inválidos',
data: error.issues // Para Zod
})
}
})
Las funciones readValidatedBody
, getValidatedQuery
y getValidatedRouterParams
de h3 hacen que validar datos en tus APIs de Nuxt sea súper simple y seguro. Con librerías como Zod o Valibot, tenés validación robusta con type safety completo en una sola línea.
Es una de esas features que una vez que las usás, no podés volver atrás. Menos código, más seguridad, mejor experiencia de desarrollo.