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
ValidationErrorearly, 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)
}
}