example

Base usage

  1. First step is to define you errors parsers. Usually you can do in utils or shared/utils directory.
import type { ValidationErrorParser } from 'nuxt-precognition'

// app/utils/precognition.ts or shared/utils/precognition.ts
export const customErrorParser: ValidationErrorParser = (error) => {
  if (error instanceof CustomError) {
    const errors: Record<string, string[]> = {}
    for (const issue of error.issues) {
      const key = issue.path.join('.')
      if (key in errors) {
        errors[key].push(issue.message)
        continue
      }
      errors[key] = [issue.message]
    }
    return { errors, message: error.message }
  }
  return null
}
  1. Add the parsers globally
// app/plugins/precognition.ts
export default defineNuxtPlugin(() => {
  const { $precognition } = useNuxtApp()

  $precognition.errorParsers.push(customErrorParser)

  // ..
})

Thats it!

From now on, the module knows how to parse ValidationErrors.

Note: Remember that global parsers will be used all times when Error is intercepted.

Example usage

Use the composable in setup method.

const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

const form = useForm(
  (): z.infer<typeof UserSchema> => ({
    email: '',
    password: '',
  }),
  (body, headers) => $fetch('/api/login', {
    method: 'POST',
    headers,
    body,
  }),
  {
    clientValidation(data) {
      UserSchema.parse(data)
    },
  },
)

function login() {
  form.submit()
}

function reset() {
  form.reset()
  document.getElementById('email')?.focus()
}
<form @submit.prevent="login" @reset.prevent="reset">
  <div>
    <label for="email">Email address</label>
    <input id="email" v-model="form.email" name="email" type="email" @change="form.validate('email')" />
    <span v-if="form.valid('email')">OK!!</span>
    <span v-if="form.invalid('email')">{{ form.errors.email }}</span>
  </div>

  <div>
    <label for="password">Password</label>
    <input
      id="password"
      v-model="form.password"
      name="password"
      type="password"
      autocomplete="current-password"
      required
      @change="form.validate('password')"
    />
    <span v-if="form.valid('password')">OK!!</span>
    <span v-if="form.invalid('password')">{{ form.errors.password }}</span>
  </div>

  <div>
    <button type="submit">Sign in</button>
    <button type="reset">Reset</button>
  </div>
</form>

Server side validation

Wait what about http errors? And how can we validate data but skipping next steps?

  1. update the default configuration.
// nuxt.config.ts

export default defineNuxtConfig({
  modules: [
    'nuxt-precognition'
  ],
  precognition: {
    backendValidation: true,
    enableNuxtClientErrorParser: true,
  },
})

Here we are instructing the module to:

  • add backend validation (precognitive-requests) when we request single key validation.
  • add global parser to translate NuxtErrors to ValidationErrors.
  1. Create a Nitro plugin to parse server errors:
// server/plugins/precognition.ts
import { ZodError } from 'zod'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', (event) => {
    event.context.$precognition.errorParsers = [
      zodErrorParser
    ]
  })
})

Assuming we are using same validation library on backend (in this example zod), we need to translate zod errors to NuxtErrors the module will understand.

  1. Use definePrecognitiveEventHandler in the object way and add validation in the onRequest hook.
import { definePrecognitiveEventHandler, readBody } from '#imports'
// server/api/login.post.ts
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string()
}).refine((_data) => {
  // Check for email and password match
  // ...
  return true
}, { message: 'invalid credentials', path: ['email'] },)

export default definePrecognitiveEventHandler({
  async onRequest(event) {
    const body = await readBody(event)
    loginSchema.parse(body)
  },
  handler: () => {
    return {
      status: 200,
      body: {
        message: 'Success',
      },
    }
  },
})

Splitting the handler in different hooks, we can isolate validation from the main functionality.

When the precognitive-request is detected, only the validation function will run.

Custom Parsers per request

It happens you have a specific apis, where you can have errors in different shapes. For example using common authentication services with specific sdks.

No problem, you can define the parser on the useForm composable.

const form = useForm(
  (): z.infer<typeof UserSchema> => ({
    email: '',
    password: '',
  }),
  (body, headers) => $fetch('/api/login', {
    method: 'POST',
    headers,
    body,
  }),
  {
    clientValidation(data) {
      UserSchema.parse(data)
    },
    clientErrorParsers: [
      (error) => {
        if (error instanceof CustomError) {
          // ....
          return { errors, message: error.message }
        }
        //
        return null
      }
    ]
  },
)

This parser will be used only for this form submission.

Same thing on backend.

export default definePrecognitiveEventHandler({
  async onRequest(event) {
    const body = await readBody(event)
    loginSchema.parse(body)
  },
  handler: () => {
    return {
      status: 200,
      body: {
        message: 'Success',
      },
    }
  },
}, {
  errorParsers: [
    (error) => {
      if (error instanceof z.ZodError) {
        return {
          message: 'Invalid data',
          errors: error.issues.map(issue => ({
            path: issue.path.join('.'),
            message: issue.message,
          }))
        }
      }
    }
  ]
})
Previous
Core