

# AWS AppSync Event API runtime reference
<a name="runtime-reference-overview"></a>

AWS AppSync lets you respond to specific triggers that occur in the service with code that runs on AWS AppSync's JavaScript runtime (APPSYNC\$1JS).

The AWS AppSync JavaScript runtime provides a subset of JavaScript libraries, utilities, and features. For a complete list of features and functionality supported by the `APPSYNC_JS` runtime, see [Runtime features](runtime-features-overview.md).

**Topics**
+ [Event handlers overview](event-handlers-overview.md)
+ [Writing event handlers](writing-event-handlers.md)
+ [Configuring utilities for the `APPSYNC_JS` runtime](configure-utilities.md)
+ [Bundling, TypeScript, and source maps for the `APPSYNC_JS` runtime](additional-utilities.md)

# Event handlers overview
<a name="event-handlers-overview"></a>

An event handler is a function defined in a namespace that is invoked by specific triggers in the system. Currently you can define onPublish and onSubscribe event handlers: handlers that respond to events being published to a channel in the namespace, and handlers that respond to subscription items on a channel in the namespace. An onPublish handler is called before events are broadcast to subscribed clients, giving you a chance to transform the events first. An onSubscribe handler is called as a client tries to subscribe, giving you the chance to accept or reject the subscription attempt.

Event handlers are optional and are not required for your channel namespaces to be effective.

# Writing event handlers
<a name="writing-event-handlers"></a>

A handler is defined by a single function that doesn't interact with a data source, or is defined by an object that implements a request and a response function to interact with one of your data sources. When working with a Lambda function data source, AWS AppSync Events supports a `DIRECT` integration that allows you to interact with your Lambda function without writing any handler code.

You provide your code for the handlers using the namespace's code property. Essentially, you use a "single file" to define all your handlers. In your code file, you identify your handler definitions by exporting a function or object named onPublish or onSubscribe.

## Handler with no data source integration
<a name="no-datasource-handler"></a>

You can define a handler with no data source integration. In this case, the handler is defined as a single function. In the following example, the onPublish handler shows the default behavior when no handler is defined. It simply forwards the list of events.

```
export function onPublish(ctx) {
  return ctx.events
}
```

As an alternate example, this definition returns an empty list, which means that no event will be broadcast to the subscribers.

```
export const onPublish = (ctx) => ([])
```

In this example, the handler only returns the list of published events, but adds the property `msg` to the payload. 

```
export function onPublish(ctx) {
  return ctx.events.map(({id, payload}) => {
    return {id: payload: {...payload, msg: "Hello!"}}
  })
}
```

## Handler with a data source integration
<a name="with-datasource-handler"></a>

You can define a handler with a data source integration. In this case, you define an object that implements and `request` and a `response` function. The `request` function defines the payload that is sent to invoke the data source while the `response` function receives the result of that invocation. The list of events to broadcast is returned by the `response` function.

The following example defines an `onPublish` handler that saves all published events to a `messages_table` before forwarding them to be broadcast. The `onSubscribe` handler doesn't have a data source integration and is defined by a single function that simply logs a message.

```
import * as ddb from `@aws-appsync/utils/dynamodb`

const TABLE = 'messages_table'
export const onPublish = {
  request(ctx) {
    const channel = ctx.info.channel.path
    return ddb.batchPut({
      tables: {
        [TABLE]: ctx.events.map(({ id, payload }) => ({ channel, id, ...payload })),
      },
    })
  },
  response(ctx) {
    console.log(`Batch Put result:`, ctx.result.data[TABLE])
    return ctx.events
  },
}

export const onSubscribe = (ctx) => {
  console.debug(`Joined the chat: ${ctx.info.channel.path}`)
}
```

## Skipping the data source
<a name="skip-datasource"></a>

You might have situations where you need to skip the data source invocation at run time. You can do this by using the `runtime.earlyReturn` utility. `earlyReturn` immediately returns the provided payload and skips the response function.

```
import * as ddb from `@aws-appsync/utils/dynamodb`

const TABLE = 'messages_table'
export const onPublish = {
  request(ctx) {
    if (ctx.info.channel.segments.includes('private')) {
      // return early and do no execute the response.
      return runtime.earlyReturn(ctx.events)
    }
    const channel = ctx.info.channel.path
    return ddb.batchPut({
      tables: {
        [TABLE]: ctx.events.map(({ id, payload }) => ({ channel, id, ...payload })),
      },
    })
  },
  response(ctx) {
    return ctx.result.data[TABLE].map(({ id, ...payload }) => ({ id, payload }))
  },
}
```

## Returning an error
<a name="return-error"></a>

During the execution of an event handler, you might need to return an error back to the publisher or subscriber. Use the `util.error` function to do this. When publishing is done using the HTTP endpoint, this returns an HTTP 403 response. When publishing over WebSocket, this returns a `publish_error` message with the provided message. The following example demonstrates how to return an error message.

```
export function onPublish(ctx) {
  util.error("Not possible!")
  return ctx.events
}
```

## Unauthorizing a request
<a name="unauthorizing-request"></a>

Your event handlers are always called after AWS AppSync has authorized the requests. However, you can run additional business logic and unauthorize a request in your event handler using the `util.unauthorize` function. When publishing over HTTP, this returns an HTTP 401 response. Over WebSocket, this returns a `publish_error` message with an `UnauthorizedException` error type. When trying to connect over WebSocket, you get a `subscribe_error` with an `Unauthorized` error type. 

```
export function onSubscribe(ctx) {
  if (somethingNotValid() === true) {
    util.unauthorized()
  }
}

function somethingNotValid() {
  // implement custom business logic
}
```

## Direct Lambda integration
<a name="direct-lambda-integration"></a>

AWS AppSync lets you integrate Lambda functions directly with your channel namespace without writing additional handler code. This integration supports both publish and subscribe operations through Request/Response mode.

**How it works**

When AWS AppSync calls your Lambda function, it passes a context object containing event information. Then, the Lambda function can perform the following operations:
+ Filter and transform events for broadcasting
+ Return error messages for failed processing
+ Handle both publish and subscribe operations

**Publish operation response format**

For `onPublish` handlers, your Lambda function must return a response payload with the following structure:

```
type LambdaAppSyncEventResponse = {
  /** Array of outgoing events to broadcast */
  events?: OutgoingEvent[],
  
  /** Optional error message if processing fails */
  error?: string
}
```

**Note**  
If you include an error message in the response, AWS AppSync logs it (when logging is enabled) but doesn't return it to the publisher.

**Subscribe operation response**

For `onSubscribe` handlers, your Lambda function must return one of the following:
+ A payload containing an error message
+ `null` to indicate a successful subscription

```
type LambdaAppSyncEventResponse = {
  /** Error message if subscription fails */
  error: string
} | null
```

**Best practices**

We recommend the following best practices for direct Lambda integrations:
+ Enable logging to track error messages.
+ Ensure your Lambda function handles both success and error cases.
+ Test your integration with various payload scenarios.

**Utilizing Powertools for Lambda**

You can utilize Powertools for Lambda to efficiently write your Lambda function handlers. To learn more, see the following Powertools for AWS Lambda documentation resources:
+ TypeScript/Node.js — See [https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events/](https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events/) in the *Powertools for AWS Lambda (TypeScript)* documentation.
+ Python — See [https://docs.powertools.aws.dev/lambda/python/latest/core/event\$1handler/appsync\$1events/](https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/appsync_events/) in the *Powertools for AWS Lambda (Python)* documentation.
+ .NET — See [https://docs.powertools.aws.dev/lambda/dotnet/core/event\$1handler/appsync\$1events/](https://docs.powertools.aws.dev/lambda/dotnet/core/event_handler/appsync_events/) in the *Powertools for AWS Lambda (.NET) * documentation.

# Configuring utilities for the `APPSYNC_JS` runtime
<a name="configure-utilities"></a>

AWS AppSync provides the following two libraries that help you develop event handlers with the `APPSYNC_JS` runtime: 
+ `@aws-appsync/eslint-plugin` - Catches and fixes problems quickly during development.
+ `@aws-appsync/utils` - Provides type validation and autocompletion in code editors.

## Configuring the eslint plugin
<a name="configure-eslint-plugin"></a>

[ESLint](https://eslint.org/) is a tool that statically analyzes your code to quickly find problems. You can run ESLint as part of your continuous integration pipeline. `@aws-appsync/eslint-plugin` is an ESLint plugin that catches invalid syntax in your code when leveraging the `APPSYNC_JS` runtime. The plugin allows you to quickly get feedback about your code during development without having to push your changes to the cloud.

`@aws-appsync/eslint-plugin` provides two rule sets that you can use during development. 

**"plugin:@aws-appsync/base"** configures a base set of rules that you can leverage in your project. The following table describes these rules.


| Rule | Description | 
| --- | --- | 
| no-async | Async processes and promises are not supported. | 
| no-await | Async processes and promises are not supported. | 
| no-classes | Classes are not supported. | 
| no-for | for is not supported (except for for-in and for-of, which are supported) | 
| no-continue | continue is not supported. | 
| no-generators | Generators are not supported. | 
| no-yield | yield is not supported. | 
| no-labels | Labels are not supported. | 
| no-this | this keyword is not supported. | 
| no-try | Try/catch structure is not supported. | 
| no-while | While loops are not supported. | 
| no-disallowed-unary-operators | \$1\$1, --, and \$1 unary operators are not allowed. | 
| no-disallowed-binary-operators | The instanceof operator is not allowed. | 
| no-promise | Async processes and promises are not supported. | 

**"plugin:@aws-appsync/recommended"** provides some additional rules but also requires you to add TypeScript configurations to your project.


| Rule | Description | 
| --- | --- | 
| no-recursion | Recursive function calls are not allowed. | 
| no-disallowed-methods | Some methods are not allowed. See the [Runtime features](runtime-features-overview.md) for a full set of supported built-in functions. | 
| no-function-passing | Passing functions as function arguments to functions is not allowed. | 
| no-function-reassign | Functions cannot be reassigned. | 
| no-function-return | Functions cannot be the return value of functions. | 

To add the plugin to your project, follow the installation and usage steps at [Getting Started with ESLint](https://eslint.org/docs/latest/user-guide/getting-started#installation-and-usage). Then, install the [plugin](https://www.npmjs.com/package/@aws-appsync/eslint-plugin) in your project using your project package manager (e.g., npm, yarn, or pnpm):

```
$ npm install @aws-appsync/eslint-plugin
```

In your `.eslintrc.{js,yml,json}` file, add **"plugin:@aws-appsync/base"** or **"plugin:@aws-appsync/recommended"** to the `extends` property. The snippet below is a basic sample `.eslintrc` configuration for JavaScript: 

```
{
  "extends": ["plugin:@aws-appsync/base"]
}
```

To use the **"plugin:@aws-appsync/recommended"** rule set, install the required dependency:

```
$ npm install -D @typescript-eslint/parser
```

Then, create an `.eslintrc.js` file:

```
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2018,
    "project": "./tsconfig.json"
  },
  "extends": ["plugin:@aws-appsync/recommended"]
}
```

# Bundling, TypeScript, and source maps for the `APPSYNC_JS` runtime
<a name="additional-utilities"></a>

TypeScript enhances AWS AppSync development by providing type safety and early error detection. You can write TypeScript code locally and transpile it to JavaScript before using it with the `APPSYNC_JS` runtime. The process starts with installing TypeScript and configuring tsconfig.json for the `APPSYNC_JS` environment. You can then use bundling tools like esbuild to compile and bundle the code. 

You can leverage custom and external libraries in your handler and function code, as long as they comply with `APPSYNC_JS` requirements. Bundling tools combine code into a single file for use in AWS AppSync. Source maps can be included to aid debugging. 

## Leveraging libraries and bundling your code
<a name="using-external-libraries"></a>

In your handler code, you can leverage both custom and external libraries so long as they comply with the `APPSYNC_JS` requirements. This makes it possible to reuse existing code in your application. To make use of libraries that are defined by multiple files, you must use a bundling tool, such as [esbuild](https://esbuild.github.io/), to combine your code in a single file that can then be saved to your AWS AppSync namespace handler code.

When bundling your code, keep the following in mind:
+ `APPSYNC_JS` only supports ECMAScript modules (ESM).
+ `@aws-appsync/*` modules are integrated into `APPSYNC_JS` and should not be bundled with your code.
+ The `APPSYNC_JS` runtime environment is similar to NodeJS in that code does not run in a browser environment.
+ You can include an optional source map. However, do not include the source content.

  To learn more about source maps, see [Using source maps](#source-maps).

For example, to bundle your handler code located at `src/appsync/onPublish.js`, you can use the following esbuild CLI command:

```
$ esbuild --bundle \
--sourcemap=inline \
--sources-content=false \
--target=esnext \
--platform=node \
--format=esm \
--external:@aws-appsync/utils \
--outdir=out/appsync \
 src/appsync/onPublish.js
```

## Building your code and working with TypeScript
<a name="working-with-typescript"></a>

[TypeScript](https://www.typescriptlang.org/) is a programming language developed by Microsoft that offers all of JavaScript’s features along with the TypeScript typing system. You can use TypeScript to write type-safe code and catch errors and bugs at build time before saving your code to AWS AppSync. The `@aws-appsync/utils` package is fully typed.

The `APPSYNC_JS` runtime doesn't support TypeScript directly. You must first transpile your TypeScript code to JavaScript code that the `APPSYNC_JS` runtime supports before saving your code to AWS AppSync. You can use TypeScript to write your code in your local integrated development environment (IDE), but note that you cannot create TypeScript code in the AWS AppSync console.

To get started, make sure you have [TypeScript](https://www.typescriptlang.org/download) installed in your project. Then, configure your TypeScript transcompilation settings to work with the `APPSYNC_JS` runtime using [TSConfig](https://www.typescriptlang.org/tsconfig). Here’s an example of a basic `tsconfig.json` file that you can use:

```
// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
   "noEmit": true,
   "moduleResolution": "node",
  }
}
```

You can then use a bundling tool like esbuild to compile and bundle your code. For example, given a project with your AWS AppSync code located at `src/appsync`, you can use the following command to compile and bundle your code:

```
$ esbuild --bundle \
--sourcemap=inline \
--sources-content=false \
--target=esnext \
--platform=node \
--format=esm \
--external:@aws-appsync/utils \
--outdir=out/appsync \
 src/appsync/**/*.ts
```

### Using generics in TypeScript
<a name="working-with-typescript-generics"></a>

You can use generics with several of the provided types. For example, you can write a handler that makes use of the `√`≈. In your IDE, type definitions and auto-complete hints will guide you into properly using the available utilities.

```
import type { EventOnPublishContext, IncomingEvent, OutgoingEvent } from "@aws-appsync/utils"
import * as ddb from '@aws-appsync/utils/dynamodb'

type Message = {
  id: string;
  text: string;
  owner: string;
  likes: number
}

type OnP<T = any> = {
  request: (ctx: EventOnPublishContext<T>) => unknown,
  response: (ctx: EventOnPublishContext<T>) => OutgoingEvent[] | IncomingEvent[]
}

export const onPublish: OnP<Message> = {
  request(ctx) {
    const msg = ctx.events[0]
    return ddb.update<Message>({
      key: { owner: msg.payload.owner, id: msg.payload.id },
      update: msg.payload,
      condition: { id: { attributeExists: true } }
    })
  },
  response: (ctx) => ctx.events
}
```

## Linting your bundles
<a name="using-lint-with-bundles"></a>

You can automatically lint your bundles by importing the `esbuild-plugin-eslint` plugin. You can then enable it by providing a `plugins` value that enables eslint capabilities. Below is a snippet that uses the esbuild JavaScript API in a file called `build.mjs`:

```
/* eslint-disable */
import { build } from 'esbuild'
import eslint from 'esbuild-plugin-eslint'
import glob from 'glob'
const files = await glob('src/**/*.ts')

await build({
  format: 'esm',
  target: 'esnext',
  platform: 'node',
  external: ['@aws-appsync/utils'],
  outdir: 'dist/',
  entryPoints: files,
  bundle: true,
  plugins: [eslint({ useEslintrc: true })],
})
```

## Using source maps
<a name="source-maps"></a>

You can provide an inline source map (`sourcemap`) with your JavaScript code. Source maps are useful for when you bundle JavaScript or TypeScript code and want to see references to your input source files in your logs and runtime JavaScript error messages.

Your `sourcemap` must appear at the end of your code. It is defined by a single comment line that follows the following format:

```
//# sourceMappingURL=data:application/json;base64,<base64 encoded string>
```

The following is an example of a source map:

```
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsibGliLmpzIiwgImNvZGUuanMiXSwKICAibWFwcGluZ3MiOiAiO0FBQU8sU0FBUyxRQUFRO0FBQ3RCLFNBQU87QUFDVDs7O0FDRE8sU0FBUyxRQUFRLEtBQUs7QUFDM0IsU0FBTyxNQUFNO0FBQ2Y7IiwKICAibmFtZXMiOiBbXQp9Cg==
```

Source maps can be created with esbuild. The example below shows you how to use the esbuild JavaScript API to include an inline source map when code is built and bundled:

```
import { build } from 'esbuild'
import eslint from 'esbuild-plugin-eslint'
import glob from 'glob'
const files = await glob('src/**/*.ts')

await build({
  sourcemap: 'inline',
  sourcesContent: false,
  
  format: 'esm',
  target: 'esnext',
  platform: 'node',
  external: ['@aws-appsync/utils'],
  outdir: 'dist/',
  entryPoints: files,
  bundle: true,
  plugins: [eslint({ useEslintrc: true })],
})
```

In the preceeding example, the `sourcemap` and `sourcesContent` options specify that a source map should be added in line at the end of each build but should not include the source content. As a convention, we recommend not including source content in your `sourcemap`. You can disable this in esbuild by setting `sources-content` to `false`.

To illustrate how source maps work, review the following example in which handler code references helper functions from a helper library. The code contains log statements in the handler code and in the helper library:

**./src/channelhandler.ts** (your handler)

```
import { EventOnPublishContext }  from "@aws-appsync/utils";
 import { mapper }  from "./lib/mapper";

 exportfunction onPublish ( ctx: EventOnPublishContext ) {
   return ctx.events.map(mapper)
}
```

**./lib/helper.ts** (a helper file)

```
import { IncomingEvent, OutgoingEvent } from "@aws-appsync/utils";

export function mapper(event: IncomingEvent, index: number) {
  console.log(`-> mapping: event ${event.id}`)
  return {
    ...event,
    payload: { ...event.payload, mapped: true },
    error: index % 2 === 0 ? 'flip flop error' : null
  } as OutgoingEvent
}
```

When you build and bundle the handler file, your handler code will include an inline source map. When your handler runs, entries will appear in the CloudWatch logs.