Developing and Testing AWS AppSync JavaScript Resolvers

Eric Bach
AMA Technology Blog
6 min readJun 5, 2023

Ever since AWS announced the support of JavaScript resolvers for AWS AppSync in November 2022 my productivity and efficiency in building GraphQL APIs has significantly improved.

AWS AppSync provides the ability to build GraphQL APIs that connect to different data sources using resolvers. These resolvers are used by AWS AppSync to translate GraphQL requests and responses from various data sources.

Photo by Jr Korpa on UnSplash

The introduction of JavaScript resolvers provides an alternative to the traditional method of building AWS AppSync resolvers with the Apache Velocity Template Language (VTL). Previously, this required learning a new language, and VTL resolvers were often cumbersome to test. Here is what it might look like retrieving an item from a DynamoDB table using VTL:

#set($tableName = "someTable")
#set($key = {
"pk": $util.dynamodb.toDynamoDBJson($ctx.args.pk)
})

{
"version": "2017-02-28",
"operation": "GetItem",
"key": $util.toJson($key),
"tableName": $tableName
}

Many developers, like myself, often resorted to writing VTL resolvers that invoked an AWS Lambda function, usually also written in JavaScript anyways. However, this was not an optimal solution as it added more complexity, performance overhead, and increased costs associated with the integration of AWS Lambda.

AppSync JavaScript Resolvers

With JavaScript resolvers, AppSync can communicate with almost any AWS service that has data sources using plain JavaScript. This includes integrations with DynamoDB, RDS, Lambda, OpenSearch, EventBridge, and HTTP endpoints. The later integration with HTTP endpoints allows for integration with virtually any available AWS service.

Supported AWS AppSync Data Sources

Using JavaScript resolvers, developing resolvers is much simpler as JavaScript is a language familiar to most developers. Additionally, leveraging Jest for testing makes the process of testing resolvers a breeze. Let’s explore how to build and test JavaScript resolvers in AWS AppSync with CDK!

Getting Started

In this example, we will demonstrate the use of AWS AppSync JavaScript resolvers with DynamoDB as a data source.

Using JavaScript Resolvers to integrate with DynamoDB
  1. First we create a DynamoDB data source that references our AppSync GraphQL API and DynamoDB table. We also define a custom service role to provide AppSync access to any indexes on the table.
const dynamoDbDataSource = new DynamoDbDataSource(this, `dynamoDbDataSource`, {
api: api, // the AppSync GraphqlApi
table: dataTable, // the DynamoDB Table
serviceRole: new Role(this, 'DynamoDbAppSyncServiceRole', {
assumedBy: new ServicePrincipal('appsync.amazonaws.com'),
inlinePolicies: {
name: new PolicyDocument({
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'dynamodb:BatchGetItem',
'dynamodb:BatchWriteItem',
'dynamodb:ConditionCheckItem',
'dynamodb:DeleteItem',
'dynamodb:DescribeTable',
'dynamodb:GetItem',
'dynamodb:GetRecords',
'dynamodb:GetShardIterator',
'dynamodb:PutItem',
'dynamodb:Query',
'dynamodb:Scan',
'dynamodb:UpdateItem',
],
// Note: '/*' to provide access to all indexes on the table
resources: [dataTable.tableArn + '/*'],
}),
],
}),
},
}),
});

⚠️Caution⚠️

By default, the AppSync Service Role created with the DynamoDB data source only has access to the main DynamoDB table. If there are any indexes (LSI/GSI) on the table, a custom AppSync Service Role must be defined with the appropriate permissions.

2. Next we define our AWS AppSync function containing the JavaScript resolver code. The name of the function matches the name of the action in our GraphQL schema.

const getItemFunction = new AppsyncFunction(this, 'getItem', {
name: 'getItem',
api: api, // the AppSync GraphqlApi
dataSource: dynamoDbDataSource, // the Data Source in step 1
code: Code.fromAsset(path.join(__dirname, '/graphql/Query.getItem.js')),
runtime: FunctionRuntime.JS_1_0_0,
});

3. The JavaScript code for the resolver itself is fairly straight forward. Here we are able to query our DynamoDB table with the partition key.

// Query.getItem.js

import { util } from '@aws-appsync/utils';

export function request(ctx) {
return {
version: '2017-02-28',
operation: 'Query',
query: {
expression: 'pk = :pk',
expressionValues: {
':pk': util.dynamodb.toDynamoDB(ctx.args.pk),
},
},
};
}

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

⚠️Caution⚠️

When using other DynamoDB operations such as, TransactionWriteItem, the table name is required. If the table name is different for each application environment there is currently no way to provide a dynamic value to the resolver. The work arounds are to write the resolver code inline in the CDK stack or provide the table name as part of the GraphQL request context. Hopefully AWS AppSync provides a solution for this issue in the near future. Below is one example of such a situation:

export function request(ctx) {
return {
operation: 'TransactWriteItems',
transactItems: [
{
// table name passed in via request conext
table: `my-${ctx.args.input.envName}-table`,
operation: 'PutItem',

// Rest of code omitted for brevity

4. Because AppSync JavaScript resolvers only support pipeline resolvers at the moment we need to include the code to support passing the contexts between each resolver (even if there is only one resolver).

const passthrough = InlineCode.fromInline(`
// The before step
export function request(...args) {
return {}
}

// The after step
export function response(ctx) {
return ctx.prev.result
}
`);

5. Finally we define our resolver that contains the single function defined earlier. As noted previously, we must create a pipeline resolver when using JavaScript so note that the pipelineConfig only contains our single resolver. If there was a need to chain multiple actions together, this is where additional resolvers would be included.

const getItemResolver = new Resolver(this, 'getItemResolver', {
api: api, // the AppSync GrapqlApi
typeName: 'Query',
fieldName: 'getItem',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [getItemFunction], // the AppSync Function in step 2
code: passthrough, // the pipeline in step 3
});

With this in place, we have all the necessary pieces to start using our AWS AppSync GraphQL endpoint with a JavaScript resolver.

Testing

Traditionally testing VTL resolvers involved some work but with the use of JavaScript, testing can be easily achieved using well known JavaScript testing libraries like Jest.

To test a resolver we use the EvaluateCode command from the AWS SDK v3 that calls AWS AppSync and evaluates the code with the provided context. We are able to quickly test for response values in the GraphQL endpoint.

import { AppSyncClient, EvaluateCodeCommand, EvaluateCodeCommandInput } from '@aws-sdk/client-appsync';
import { unmarshall } from '@aws-sdk/util-dynamodb';
import { readFile } from 'fs/promises';
const appsync = new AppSyncClient({ region: 'us-east-1' });
const file = './lib/graphql/Query.getItem.js';

describe('getItem', () => {
it('returns and item from the partition key', async () => {
// Arrange
const context = {
arguments: {
pk: '123',
},
};
const input: EvaluateCodeCommandInput = {
runtime: { name: 'APPSYNC_JS', runtimeVersion: '1.0.0' },
code: await readFile(file, { encoding: 'utf8' }),
context: JSON.stringify(context),
function: 'request',
};
const evaluateCodeCommand = new EvaluateCodeCommand(input);

// Act
const response = await appsync.send(evaluateCodeCommand);

// Assert
expect(response).toBeDefined();
expect(response.error).toBeUndefined();
expect(response.evaluationResult).toBeDefined();

const result = JSON.parse(response.evaluationResult ?? '{}');
expect(result.operation).toEqual('Query');
expect(result.query.expression).toEqual('pk = :pk');

const expressionValues = unmarshall(result.query.expressionValues);
expect(expressionValues[':pk']).toEqual(context.arguments.pk);
});
});

Benefits of JavaScript Resolvers

The introduction of JavaScript resolvers has brought many important improvements with the development workflow along with some limitations.

  • Improved developer experience — by providing a well-known, familiar JavaScript development experience, complete with IDE support, and code linting through the use of @aws-appsync/eslint-plugin
  • Ease of testing — through the use of the same JavaScript libraries that exist today, like Jest
  • Typing support — with TypeScript, provided the code is transpiled to JavaScript prior to deployment
  • Clean code — development of reusable code or custom packages in the large JavaScript ecosystem
  • No more cold starts — since it is a direct integration with the data source, requiring no Lambda functions (excluding Lambda Provisioned Concurrency)
  • Free— built in as part of AWS AppSync so it comes with no additional cost

Limitations of JavaScript Resolvers

  • Unsupported JavaScript features — with the use of the APPSYNC_JS runtime (a limited subset of ECMAScript 6) many JavaScript features like loops, try-catch, throw errors, arrow functions, async/await are not supported. See the runtime documentation for more details.
  • No environment variables — there is no ability to pass environment variables to a JavaScript resolver like AWS Lambda. For example, using DynamoDB as a data source may require providing the TableName, which can be different or each application environment.
  • Limited package support — inability to import any available npm package that is not APPSYNC_JS runtime compatible or produces a package size greater than 32KB. Perhaps this would be a great possibility for the creation of custom appsync-compatible libraries?
  • Pipeline resolvers only — to have a single Unit resolver use VTL or create a Pipeline resolver with one function (like our example)

Summing Up

For a more detailed comparison between AWS AppSync JavaScript resolvers and Lambda as a data source, see the AWS comparison.

The support for JavaScript resolvers in AWS AppSync has made development of GraphQL APIs much easier. If you have enjoyed this article, in the next series we will look into how to use the HTTP endpoint data source as well as direct integration to EventBridge.

Eric Bach is a Senior Software Developer @Alberta Motor Association who enjoys learning, reading, and writing about leadership principles, event-driven microservices, and all things AWS.

--

--

Eric Bach
AMA Technology Blog

Senior Software Developer @ amaabca | AWS Certified x 2 | Domain Driven Design | Event Driven Architecture | CQRS | Microservices