appsync

AppSync Lambda Functions

For complex form validation—such as checking unique database fields or running heavy business logic—you should delegate validation to an AWS Lambda function attached to your AppSync resolver.

How AppSync Invokes Lambda

When AppSync invokes a Lambda resolver, it passes the context object as the Lambda event payload. Within this payload, you can access:

  • event.arguments.input — the user input arguments.
  • event.request.headers — the HTTP headers sent by the client.

Implementing Validation in Lambda

Inside your Lambda function, you can use any Node.js validation library (like Zod, Yup, or Valibot) to parse and validate the input arguments.

Lambda Code Example (Node.js + Zod)

Here is a typical AWS Lambda handler that validates a request using Zod:

import { Handler } from 'aws-lambda'
import { z } from 'zod'

// 1. Define the validation schema
const RegisterSchema = z.object({
  email: z.string().email('Invalid email address'),
  username: z.string().min(3, 'Username must be at least 3 characters'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

export const handler: Handler = async (event) => {
  const input = event.arguments.input
  const headers = event.request.headers || {}
  const isPrecognitive = headers.precognitive === 'true'
  const validateOnly = headers['precognition-validate-only']
    ? headers['precognition-validate-only'].split(',')
    : []

  // 2. Perform validation
  const result = RegisterSchema.safeParse(input)

  if (!result.success) {
    // Format Zod errors to { fieldName: [message1, message2] }
    const validationErrors: Record<string, string[]> = {}

    result.error.errors.forEach((err) => {
      const field = err.path.join('.')
      if (!validationErrors[field]) {
        validationErrors[field] = []
      }
      validationErrors[field].push(err.message)
    })

    // Filter validation errors based on ValidateOnly header
    const filteredErrors: Record<string, string[]> = {}
    if (validateOnly.length > 0) {
      for (const field of validateOnly) {
        if (validationErrors[field]) {
          filteredErrors[field] = validationErrors[field]
        }
      }
    }
    else {
      Object.assign(filteredErrors, validationErrors)
    }

    // 3. Throw validation error if there are issues
    if (Object.keys(filteredErrors).length > 0) {
      throw new Error(JSON.stringify({
        errorType: 'ValidationError',
        validationErrors: filteredErrors
      }))
    }
  }

  // 4. Early exit for precognitive requests
  if (isPrecognitive) {
    throw new Error(JSON.stringify({
      errorType: 'PrecognitionSuccess'
    }))
  }

  // 5. Normal processing (e.g. save to DB, create user)
  // ...

  return {
    id: 'user_123',
    email: input.email,
    username: input.username
  }
}

AppSync Resolver Configuration

If the Lambda throws an error using throw new Error(), AWS AppSync will automatically capture the error message. You can use standard VTL or JS resolver response mapping to parse the JSON string and return a structured GraphQL error payload to the client.

Resolver Response Handler (APPSYNC_JS)

export function response(ctx) {
  if (ctx.error) {
    // Attempt to parse validation errors thrown by Lambda
    try {
      const parsedError = JSON.parse(ctx.error.message)
      if (parsedError.errorType === 'ValidationError') {
        util.error(
          'Validation Failed',
          'ValidationError',
          null,
          { validationErrors: parsedError.validationErrors }
        )
      }
      if (parsedError.errorType === 'PrecognitionSuccess') {
        util.error(
          'Precognition Success',
          'PrecognitionSuccess'
        )
      }
    }
    catch (e) {
      // Fallback for non-JSON errors
      util.error(ctx.error.message, ctx.error.type)
    }
  }

  return ctx.result
}

Benefits of Lambda Validation

Using Lambda for Precognition validation offers several advantages:

  1. Reusability: Share validation schemas between your frontend, API gateway, and backend handlers.
  2. Database Checks: Run dynamic validation rules (e.g., querying DynamoDB to check if a username or email is already registered) before completing the request.
  3. Advanced libraries: Access NPM validation ecosystems (like Zod) which are not fully supported or are heavier to run inside local AppSync resolver environments.