appsync

AppSync JS Resolvers

AWS AppSync supports writing custom resolver functions in JavaScript using the APPSYNC_JS runtime. You can perform fast, in-resolver precognitive validation using @sot1986/appsync-precognition.

!IMPORTANT This validation strategy applies only to custom mutations with custom resolvers. It is not compatible with default Amplify-generated model mutations.

Bundling & Setup

The APPSYNC_JS runtime is sandboxed and enforces a 32KB code size limit. External packages cannot be imported dynamically; they must be bundled into your resolver files with dead-code elimination (DCE) while excluding native packages like @aws-appsync/utils. We recommend using tsdown.

1. Installation

Install the library and bundling devDependencies in your backend project:

# Validation library
pnpm add @sot1986/appsync-precognition

# Bundler and TypeScript
pnpm add -D tsdown typescript

2. TSConfig Configuration

Create or update tsconfig.json inside amplify/data:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true
  },
  "include": [
    "resolvers/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

3. tsdown Bundler Configuration

Create tsdown.resolvers.config.ts in amplify/data to bundle resolver files under resolvers/**/*handler.ts:

import { defineConfig } from 'tsdown'

export default defineConfig({
  target: 'esnext',
  platform: 'node',
  format: 'esm',
  deps: {
    // Only bundle the precognition library
    onlyBundle: ['@sot1986/appsync-precognition'],
    // Keep native AppSync utilities external
    neverBundle: ['@aws-appsync/utils'],
  },
  tsconfig: 'tsconfig.json',
  logLevel: 'info',
  clean: false,
  outExtensions: () => ({
    js: '.js',
  }),
  minify: 'dce-only',
  entry: {
    'resolvers/*': [
      './resolvers/**/*handler.ts',
    ],
  },
})

How It Works

Precognitive validation acts as a guard before database operations:

  • Precognitive Request (Valid): Returns early (earlyReturn(null)) with no database side-effects.
  • Validation Failure: Throws validation errors immediately.
  • Normal Mutation: Passes validated data forward to be saved.

Unit Resolver Example

Call precognitiveValidation at the start of your request function:

import { util } from '@aws-appsync/utils'
import { precognitiveValidation } from '@sot1986/appsync-precognition'

export function request(ctx) {
  // Validate inputs (halts automatically on precognition or failure)
  const validatedInput = precognitiveValidation(ctx, {
    username: ['required', ['min', 3]],
    email: ['required', 'email'],
    password: ['required', ['min', 8]],
  })

  // Normal write operation
  return {
    operation: 'PutItem',
    key: util.dynamodb.toMapValues({ id: util.autoId() }),
    attributeValues: util.dynamodb.toMapValues(validatedInput),
  }
}

export function response(ctx) {
  return ctx.result
}

Pipeline Resolver Example

In pipeline resolvers, perform validation in the first function and assert it before executing database operations in subsequent functions.

Step 1: Validation Handler

import { precognitiveValidation } from '@sot1986/appsync-precognition'

export function request(ctx) {
  return {}
}

export function response(ctx) {
  // Validates inputs and stashes them in ctx.stash.__validated
  precognitiveValidation(ctx, {
    title: ['required', ['max', 100]],
    description: ['nullable', 'string', ['max', 1000]],
  })
}

Step 2: Database Operation Handler

import { util } from '@aws-appsync/utils'
import { assertValidated } from '@sot1986/appsync-precognition'

export function request(ctx) {
  // Verifies validation ran in a previous pipeline step
  assertValidated(ctx)

  const validated = ctx.stash.__validated

  return {
    operation: 'PutItem',
    key: util.dynamodb.toMapValues({ id: validated.id }),
    attributeValues: util.dynamodb.toMapValues(validated),
  }
}

export function response(ctx) {
  return ctx.result
}

Nested Objects & Array Validation

When validating complex payloads containing arrays of nested objects, use Array.prototype.reduce to dynamically generate dot-notation validation rules for each item:

import type { Context } from '@aws-appsync/utils'
import type { Schema } from '../../resource'
import { precognitiveValidation } from '@sot1986/appsync-precognition'

export function request(ctx: Context<Schema['createUser']['args']>) {
  precognitiveValidation(ctx, {
    name: ['required', 'string', ['max', 100]],
    addresses: ['required', 'array', ['min', 1]],

    // Dynamically unroll rules for each nested address using reduce
    ...ctx.args.addresses.reduce((acc, _, idx) => ({
      ...acc,
      [`addresses.${idx}.line1`]: ['required', 'string', ['max', 255]],
      [`addresses.${idx}.line2`]: ['nullable', 'string', ['max', 255]],
      [`addresses.${idx}.city`]: ['required', 'string', ['max', 100]],
      [`addresses.${idx}.zipCode`]: ['required', 'string', ['max', 20]],
    }), {}),
  })

  return {
    // Proceed with database operation...
  }
}