Organizing How to Use Data Source Integrations in AWS AppSync Events
Back to Top
To reach a broader audience, this article has been translated from Japanese.
You can find the original version here.
Last year (2024), I introduced AWS AppSync’s Event API in the following article.
AppSync is famous for GraphQL, but with the introduction of the WebSocket-based Event API, real-time communication has become even simpler. About half a year has passed since then, and the Event API has continued to gain new features.
Representative updates include:
- What's New with AWS - AWS AppSync releases CDK L2 constructs to simplify creating WebSocket APIs
- What's New with AWS - AppSync Events adds publishing over WebSocket for real-time pub/sub
Furthermore, last month, just like with GraphQL APIs, the Event API gained the ability to integrate directly with Lambda, DynamoDB, Bedrock, and more.
According to the official documentation, the following data sources are supported at this time:
- Lambda
- DynamoDB
- RDS
- EventBridge
- OpenSearch Service
- HTTP endpoints
- Bedrock
Here, we will focus on Lambda and DynamoDB and organize how to integrate these data sources with the Event API.
Prerequisites
#First, prepare an AppSync Events environment with the minimum configuration that does not use data source integrations.
Recently, the Event API has also been supported by AWS CDK L2 constructs.
This time, instead of a CloudFormation template, we'll use CDK to build the environment. The CDK script is as follows:
import * as cdk from 'aws-cdk-lib';
import { CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import { AppSyncFieldLogLevel } from 'aws-cdk-lib/aws-appsync';
import * as logs from 'aws-cdk-lib/aws-logs';
export class AppSyncStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const apiKeyProvider: appsync.AppSyncAuthProvider = {
authorizationType: appsync.AppSyncAuthorizationType.API_KEY
};
const api = new appsync.EventApi(this, 'EventApi', {
apiName: 'AppSyncEventsSampleApi',
authorizationConfig: {
authProviders: [
apiKeyProvider
],
connectionAuthModeTypes: [
appsync.AppSyncAuthorizationType.API_KEY
],
defaultPublishAuthModeTypes: [
appsync.AppSyncAuthorizationType.API_KEY
],
defaultSubscribeAuthModeTypes: [
appsync.AppSyncAuthorizationType.API_KEY
]
},
logConfig: {
retention: logs.RetentionDays.ONE_DAY,
fieldLogLevel: AppSyncFieldLogLevel.ALL
}
});
// Channel without data source integration
api.addChannelNamespace('sample');
}
}
This is a simple configuration that uses only API Key authentication.
When you deploy this, the AppSync management console will display as follows:
Use the built-in Pub/Sub editor in the console to verify the behavior.
-
Publish event (Publish section)
You can choose HTTP or WebSocket; either works. On success, the result appears on the right.
-
Verify event delivery (Subscribe section)
If you see the event you just published on the client side, it's successful.
You can confirm that the published event is delivered unchanged to clients subscribed to the channel.
From here on, we'll add data source integrations to this AppSync Event API.
Integrating with DynamoDB (Custom Handler)
#To use DynamoDB as a data source, you need to provide an event handler specific to the Event API. The handler runs on the APPSYNC_JS runtime (a custom JavaScript execution environment).
Here, we will implement a handler that saves events published on a channel in a specified namespace to DynamoDB, processes the write results, and delivers them to clients.
Resource setup (CDK)
#First, create a DynamoDB table and register it as a data source for the Event API.
// (omitted)
// DynamoDB table for data source integration
const table = new dynamodb.Table(this, 'EventTable', {
tableName: 'AppSyncEventsTable',
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
});
// Register as data source for AppSync Events
const dynamodbDataSource = api.addDynamoDbDataSource('EventDataSource', table);
Next, add the namespace (sample-dynamodb) that will use DynamoDB, and link the previously created data source and a custom handler.
// Namespace for DynamoDB
api.addChannelNamespace('sample-dynamodb', {
// Set DynamoDB as data source
publishHandlerConfig: {
dataSource: dynamodbDataSource
},
// Custom handler
code: appsync.Code.fromInline(`
import * as ddb from '@aws-appsync/utils/dynamodb'
// Hook executed on event publish/deliver
export const onPublish = {
// Publish
request(ctx) {
const channel = ctx.info.channel.path
return ddb.batchPut({
tables: {
'${table.tableName}': ctx.events.map(({ id, payload }) => ({ channel, id, ...payload })),
}
})
},
// Deliver
response(ctx) {
// ctx.result contains the result of the request execution
return ctx.result.data['${table.tableName}'].map(({ id, ...payload }) => ({ id, payload: 'DynamoDB:' + payload.message }))
}
}
// Hook executed when a client subscribes to a channel
export const onSubscribe = (ctx) => {
console.log('Joined:' + ctx.info.channel.path)
}`)
});
The event handler registered in a namespace for AppSync Events provides two types of hooks:
Hook | Timing | Main Purpose |
---|---|---|
onPublish | When an event is published (delivered to clients) | Data source operations, event validation/transformation |
onSubscribe | When a client subscribes to a channel | Authorization, logging, initialization |
When data source integrations are enabled, each hook must be an object composed of the following functions:
- request: Perform input validation, authorization checks, and transformation for data source calls.
- response: Receive the data source execution results and handle errors or formatting.
In the code above, since we specify the DynamoDB data source in publishHandlerConfig, we implement request and response in the onPublish hook. The request function uses the built-in DynamoDB module provided by the AppSync runtime (APPSYNC_JS) to write to the table. The response function adds a prefix (DynamoDB:
) to the write results before delivering them to clients.
For simplicity, we are creating the event handler inline within AWS CDK here, but in a production environment, you probably wouldn’t do it this way. As your event handler implementation becomes more complex, you’ll want type support and linting from TypeScript (APPSYNC_JS is a fairly restrictive JavaScript runtime environment).
AppSync provides type definitions, utilities, and eslint rules as an NPM package. The official documentation below explains detailed usage steps and how to bundle with esbuild, so please refer to it:
Verifying Operation
#Next, check that the DynamoDB integration works as expected.
After deploying the stack, open the AppSync management console and verify the configuration.
You can see the DynamoDB table added as a data source and linked to the sample-dynamodb namespace. The initial sample namespace has the handler set to None (unspecified), but sample-dynamodb is configured with AppSyncJS. Expanding the details shows the source code of the event handler you created earlier.
Next, use the Pub/Sub editor to test the actual flow (connection step omitted).
-
Subscribe to channel (Subscribe section)
This time, subscribe by specifying the namespace created for DynamoDB (sample-dynamodb).
-
Publish event (Publish section)
Publish an event to the channel in the same namespace.
-
Verify event delivery (Subscribe section)
As described in the response function of the event handler, the event is delivered with the prefix (DynamoDB:
).
Finally, open the DynamoDB table and confirm that the events are stored as records.
This confirms that the data source integration with DynamoDB is functioning correctly.
Integrating Directly with Lambda
#Next, let’s use Lambda as a data source. Unlike in the DynamoDB case, it’s possible to directly integrate a Lambda function itself as an AppSync event handler. AppSync’s custom JavaScript runtime environment has various constraints[1], but by using a Node-based Lambda function, you can implement highly flexible code without these restrictions.
Here, we’ll try this direct integration.
Resource setup (CDK)
#Add the following code to your CDK script:
// Lambda function for data source integration
const fn = new lambda.Function(this, 'EventHandler', {
functionName: 'SampleAppSyncDataSourceHandler',
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
if (event.info.operation === 'PUBLISH') {
// onPublish event
return {
events: event.events.map(e => ({
id: e.id,
payload: 'Lambda:' + e.payload.message // Set the delivery content here
}))
// On error, return:
// error: 'error message'
};
} else {
// onSubscribe event
return null; // Subscription OK
}
}`)
});
// Register as data source for AppSync Events
const lambdaDataSource = api.addLambdaDataSource('EventHandler', fn);
// Namespace for Lambda
api.addChannelNamespace('sample-lambda', {
publishHandlerConfig: {
direct: true, // Direct integration
dataSource: lambdaDataSource
},
subscribeHandlerConfig: {
direct: true, // Direct integration
dataSource: lambdaDataSource
}
});
First, define a Lambda function that will serve as the event handler for AppSync Events. In this direct integration method, the Lambda function itself acts as the handler and must return a response in the format required by AppSync Events.
The official documentation describes the details, but here we implement a Lambda function that supports both onPublish and onSubscribe events. The feature of direct integration is that, unlike AppSync’s custom handlers where you need to define request/response functions, you simply return the delivery content (events) as the return value. In this example, like the DynamoDB integration, we add a prefix (Lambda:
) to the message before delivering the event.
After creating the Lambda function, configure the data source integration (addLambdaDataSource) and the namespace (sample-lambda). Unlike in the DynamoDB example, here we do not set AppSync’s custom handler in the namespace; instead, we set the direct parameter to true
in publishHandlerConfig/subscribeHandlerConfig to enable direct integration with the Lambda function.
Although we did not use it here, when implementing AppSync Events handlers in Lambda, you can achieve an intuitive implementation by using AWS Powertools provided by AWS.
Verifying Operation
#After deploying the stack, the AppSync management console will display as follows:
You can see the Lambda function registered as a data source and linked to the sample-lambda namespace. Also, the event handler setting is Direct, indicating that direct integration with the Lambda function is enabled.
Now, let’s use the Pub/Sub editor here as well to verify the behavior.
-
Subscribe to channel (Subscribe section)
Subscribe to the channel by specifying the namespace created for Lambda (sample-lambda).
-
Publish event (Publish section)
Publish an event to the channel in the same namespace.
-
Verify event delivery (Subscribe section)
You can confirm that the event is delivered with the prefix (Lambda:
) set by the Lambda function.
This confirms that direct integration with Lambda processes and delivers events as expected.
Conclusion
#In this article, we covered the newly added data source integration feature in AppSync Events by walking through basic usage examples for DynamoDB and Lambda. This feature addition makes it extremely easy to integrate with various AWS services and external resources, greatly enhancing the practical utility of AppSync Events. The usage is intuitive, and setting up the environment with CDK is relatively straightforward.
Moving forward, I plan to explore other data source integrations like RDS and Bedrock, and uncover new possibilities for real-time applications.
https://docs.aws.amazon.com/appsync/latest/eventapi/runtime-supported-features.html ↩︎