appsync

AppSync JS Resolvers

AWS AppSync allows you to write resolver functions in JavaScript using the APPSYNC_JS runtime. To implement precognitive validation seamlessly, you can use the @sot1986/appsync-precognition library.

!IMPORTANT This validation strategy is designed and works only for custom mutations where you write custom resolver handlers. It is not compatible with the default mutations automatically generated by Amplify models.

Bundling & tsdown

Because AWS AppSync executes resolver files in a sandboxed runtime environment, external Node.js dependencies cannot be loaded dynamically at runtime. You must bundle @sot1986/appsync-precognition directly into your resolver source files during the build/bundling step, while marking native utilities (like @aws-appsync/utils) as external/excluded.

For this bundling task, we use tsdown, a fast TypeScript bundler developed by members of the Vite team.

Additionally, AWS AppSync enforces a 32KB code size limit for JS Resolvers. Using a bundler like tsdown with dead-code elimination ensures your bundled resolver fits within this limit.

Installation & Setup

Install the precognition library and the bundling devDependencies inside your Amplify backend project:

# Install the validation library
pnpm add @sot1986/appsync-precognition

# Install tsdown and TypeScript for bundling
pnpm add -D tsdown typescript

1. TSConfig Configuration

Create or update the tsconfig.json file inside your amplify/data folder to compile resolver files properly:

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

2. tsdown Configuration

Create a tsdown.resolvers.config.ts configuration file under your amplify/data directory.

This configuration assumes that all resolver files are located under the amplify/data/resolvers/ directory, targeting files matching the *handler.ts pattern:

import { defineConfig } from 'tsdown'

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

Middleware Behavior & Resolver Order

Precognitive validation acts as a middleware (similar to Laravel's validation middleware). It must execute before any write operations or business logic are performed on your database.

This ensures that:

  • If the request is precognitive and validation succeeds, it will immediately exit early via runtime.earlyReturn(null), preventing any side effects.
  • If validation fails, the resolver throws a ValidationError early, blocking downstream operations.

For single/unit resolvers, always call precognitiveValidation at the very start of the request handler before returning the write operation (e.g., a DynamoDB PutItem or UpdateItem).

For multi-step pipeline resolvers, place the validation check in the first function of the pipeline (often alongside authorization) to ensure it executes before later pipeline functions perform database writes.

JS Resolver Example

Here is an example of an APPSYNC_JS pipeline resolver function that validates a signup form.

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

export function request(ctx) {
  // Validate request inputs and handle precognitive flows
  const validatedInput = precognitiveValidation(ctx, {
    username: ['required', ['min', 3]],
    email: ['required', 'email'],
    password: ['required', ['min', 8]]
  })

  // Normal execution (if it wasn't a precognitive run or validation succeeded)
  return {
    operation: 'PutItem',
    key: util.dynamodb.toMapValues({ id: util.autoId() }),
    attributeValues: util.dynamodb.toMapValues(validatedInput)
  }
}

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

Pipeline Resolvers & assertValidated

In pipeline resolvers, you typically validate the arguments in the first handler. Subsequent handlers in the pipeline can assert that validation occurred before executing database requests or business logic.

The library exports assertValidated(ctx), which verifies that ctx.stash.__validated is present. If it is not present (e.g., validation was bypassed), it will immediately raise an error.

1. Authorization & Validation (First Handler)

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

export function request(ctx) {
  // Perform authorization middleware checks...
  
  return {
    // optional query to fetch parent resource
  }
}

export function response(ctx) {
  // Perform validation on inputs
  precognitiveValidation(ctx, {
    title: ['required', ['max', 100]],
    description: ['nullable', 'string', ['max', 1000]]
  })
}

2. Database Action (Subsequent Handler)

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

export function request(ctx) {
  // Assert validation has run successfully in a previous handler
  assertValidated(ctx)

  // Use the validated arguments safely
  const validated = ctx.stash.__validated

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