appsync
Client-Side Integration
For Nuxt Precognition's useForm to detect validation failures and populate the form's error state, the submission callback must throw an exception when validation errors are present.
GraphQL endpoints typically return a standard HTTP 200 OK status code even when execution or validation encounters an error, returning the error details inside the errors array in the response payload.
Because both AppSync JS Resolvers and AWS Lambda Resolvers (via @sot1986/appsync-precognition/middy) output the same unified AppSync error structure (errorType: 'ValidationError' and errorInfo: { path, value }), the client manages both resolver types in the exact same, consistent way.
Architecture Overview
The client-side integration consists of four main parts:
- GraphQL Error Utility &
ValidationErrorClass: A parser and error class that extract field paths and messages from AppSync GraphQL errors. - Amplify Client Plugin (
01.amplify.client.ts): A Nuxt plugin wrapping the AWS Amplify GraphQL client in a Proxy to intercepterrorsand throwValidationError. - Precognition Parser Plugin (
02.precognitionParser.ts): Registers the GraphQL and Zod error parsers with Nuxt Precognition. - Form Composable Usage: Using
useFormto forward precognitive headers to your GraphQL mutations.
1. Error Parser Utility & ValidationError Class
Create a utility file that defines the ValidationError class and the graphQlValidation parser to extract validation errors from AppSync responses:
import * as z from 'zod'
export interface GraphQlFormattedError {
message: string
errorType?: string
data?: any
errorInfo?: Record<string, unknown> | null
}
export class ValidationError extends Error {
constructor(message: string, issues: GraphQlFormattedError[]) {
super(message)
this.name = 'ValidationError'
this.cause = issues
}
get issues(): GraphQlFormattedError[] {
return this.cause as GraphQlFormattedError[]
}
}
// 1. Parser for Client-Side Zod Validation Errors
export function zodErrorParser(error: Error) {
if (error instanceof z.ZodError) {
const errors = {} as Record<string, string[]>
error.issues.forEach((value) => {
const key = value.path.join('.')
if (key in errors) {
(errors[key] as string[]).push(value.message)
return
}
errors[key] = [value.message]
})
return { errors, message: 'Validation error' }
}
return null
}
// 2. Parser for AppSync GraphQL Validation Errors (Both JS Resolvers & Lambdas)
export function graphQlValidation(error: Error) {
if (error instanceof ValidationError) {
const errors = {} as Record<string, string[]>
error.issues.forEach((issue) => {
const key = typeof issue.errorInfo?.path === 'string'
? issue.errorInfo?.path
: Array.isArray(issue.errorInfo?.path)
? issue.errorInfo.path.join('.')
: null
if (!key)
return
if (key in errors) {
(errors[key] as string[]).push(issue.message)
return
}
errors[key] = [issue.message]
})
return { errors, message: error.message || 'Validation error' }
}
return null
}
// 3. Central error dispatcher for AppSync responses
export function handleAppSyncGraphQlErrors(errors: GraphQlFormattedError[]): never {
const firstError = errors.at(0)
switch (firstError?.errorType) {
case 'ValidationError':
// Throws ValidationError so Precognition's useForm can catch and parse it
throw new ValidationError(firstError.message, errors)
case 'ResourceNotFound':
throw createError({ statusCode: 404, statusMessage: firstError.message })
case 'Unauthorized':
throw createError({ statusCode: 401, statusMessage: firstError.message })
default:
throw createError({ statusCode: 500, statusMessage: firstError?.message || 'GraphQL Error' })
}
}
2. Amplify Client Plugin (01.amplify.client.ts)
Create a Nuxt plugin that initializes AWS Amplify and wraps the GraphQL client mutations and queries with a Proxy. When AppSync returns GraphQL errors, the proxy intercepts them and calls handleAppSyncGraphQlErrors:
import type { Schema } from '@amplify-data-schema'
import outputs from '#amplify-outputs'
import { Amplify } from 'aws-amplify'
import { generateClient } from 'aws-amplify/api'
import { handleAppSyncGraphQlErrors } from '~/utils/zodParser'
export default defineNuxtPlugin({
name: 'AmplifyClientPlugin',
enforce: 'pre',
setup() {
Amplify.configure(outputs, { ssr: false })
const rawClient = generateClient<Schema>({
authMode: 'userPool',
})
// Proxy client mutations to intercept GraphQL errors
const client = {
...rawClient,
mutations: new Proxy(rawClient.mutations, {
get(target, prop) {
if (typeof target[prop as keyof typeof target] !== 'function') {
return target[prop as keyof typeof target]
}
return async (...input: any[]) => {
const result = await (target[prop as keyof typeof target] as any)(...input)
if (result.errors?.length) {
handleAppSyncGraphQlErrors(result.errors)
}
return result
}
},
}),
queries: new Proxy(rawClient.queries, {
get(target, prop) {
if (typeof target[prop as keyof typeof target] !== 'function') {
return target[prop as keyof typeof target]
}
return async (...input: any[]) => {
const result = await (target[prop as keyof typeof target] as any)(...input)
if (result.errors?.length) {
handleAppSyncGraphQlErrors(result.errors)
}
return result
}
},
}),
}
return {
provide: {
Amplify: {
GraphQL: {
client,
},
},
},
}
},
})
3. Registering the Precognition Parser (02.precognitionParser.ts)
Register the graphQlValidation and zodErrorParser handlers in Nuxt Precognition during app initialization:
import { graphQlValidation, zodErrorParser } from '~/utils/zodParser'
export default defineNuxtPlugin({
name: 'PrecognitionParser',
setup() {
const { $precognition } = useNuxtApp()
$precognition.errorParsers.push(graphQlValidation, zodErrorParser)
},
})
4. Using in Forms with useForm
In your form composables or page components, use Nuxt Precognition's useForm and pass the second argument (headers) directly to the GraphQL mutation options. This automatically sends Precognition: true and Precognition-Validate-Only: <field> headers when validating individual fields:
import { useForm } from 'nuxt-precognition'
export function useRegisterForm() {
const { $Amplify } = useNuxtApp()
const form = useForm(
() => ({
email: '',
name: '',
password: '',
}),
(data, headers) => {
return $Amplify.GraphQL.client.mutations.createUser({
email: data.email,
name: data.name,
password: data.password,
}, {
// Forward Precognition headers to the AppSync GraphQL request
headers,
})
},
)
return { form }
}
<script setup lang="ts">
const { form } = useRegisterForm()
</script>
<template>
<form @submit.prevent="form.submit()">
<div>
<label>Email</label>
<input
v-model="form.email"
type="email"
@change="form.validate('email')"
>
<span v-if="form.invalid('email')" class="text-red-500 text-sm">
{{ form.errors.email }}
</span>
</div>
<div>
<label>Name</label>
<input
v-model="form.name"
type="text"
@change="form.validate('name')"
>
<span v-if="form.invalid('name')" class="text-red-500 text-sm">
{{ form.errors.name }}
</span>
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Submitting...' : 'Register' }}
</button>
</form>
</template>