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...
}
}