Mastering the Basics of AppSync JS Resolvers: A Quick Start

Mastering the Basics of AppSync JS Resolvers: A Quick Start

Key insights I wish I had received before embarking on AppSync JS Resolvers

TL;DR;

This article provides an overview of the basics of AppSync APIs, including the recommended stack of Serverless Framework, Serverless AppSync Plugin, and GraphBolt. It explains the structure of resolvers, the use of ctx and stash objects, size limits for inline resolvers and resolvers uploaded to S3, and the use of variable substitutions for environment variables. It also covers how to refactor code and use it as a JS resolver for AppSync, and how to use GraphBolt for executing API requests and debugging resolvers.

Introduction

Getting started with AppSync APIs sounds like a daunting task, especially for developers used to developing REST APIs. There are a few new concepts that you'll need to learn and understand like the usage of data sources, Resolvers, Pipeline resolvers and how to set up a proper GraphQL schema.

In this article, we will expect that the basic knowledge is already covered, for example by heading to and reading the official documentation, and we will cover what we think will allow you to get started as fast as possible while still avoiding some common mistakes.

Why JavaScript Resolvers?

While developing AppSync APIs you have a few options to choose from to implement your logic, but we will focus on JavaScript resolvers since:

  • They are the fastest way to get started

  • Most of the developers will already have previous JS knowledge

As a little disclaimer, the JS resolvers are executed in a custom runtime called APPSYNC_JS where not all JS operations are supported, but we'll dive into it later.

Another option would be to go with VTL Mapping Template resolvers, but this would mean entering a rabbit hole that might be too overwhelming for developers trying to get started with AppSync.

Recommended Stack

With simplicity and future-proofness as our main goals, the stack to recommend would always be:

  • Serverless Framework: My go-to IaC framework for developing serverless apps on AWS. This framework allows for an easy and fast declaration for every infrastructure or resource your project might need, and, for the ones that aren't as easy to set up, it allows for third-party plugins to make your life easier.

  • Serverless AppSync Plugin: Plugin used to simplify the declaration and set up of your AppSync API.

  • GraphBolt: The best tool I've been able to find so far to debug and test AppSync resolvers and APIs.

For an example of how to get started with both Serverless Framework and the Serverless AppSync Plugin, you can head to the project's readme quick start guide.

Basic Concepts

We won't go into much detail on the different concepts, but as a quick refresher of the basics:

  • Data Sources: As the name implies, it is the reference to where the resolver will be fetching the data from.

  • Resolvers: The connector between AppSync/GraphQL and the data source, aka. the file with the logic to be executed.

  • Pipeline resolvers or functions: A set of resolvers to be executed in the predefined order to build the expected response.

Everything you need to know

Time to go right to the point, here are all the things I wish someone had explained to me before getting started and would have saved me a lot of time and headaches.

Resolver Structure: request(ctx) and response(ctx)

The most important thing to understand, and that you might already know, is the resolver structure. Resolvers tell AWS AppSync how to translate an incoming GraphQL request into instructions for your backend data source, and how to translate the response from that data source back into a GraphQL response.

Having this in mind, it starts to make sense that we're expected (and forced) to export two functions in our code:

  • request(ctx): This will be the first function to be executed and should be used to verify the input and prepare or map the request parameters to be sent in the request made to the defined Data Source.

  • response(ctx): This function will be called after the Data Source has been called and should be used to verify the received response and to map it to the expected schema needed for the response.

Everyone needs a bit of context

ctx, aka. context is what AppSync will provide when calling your resolver and it will contain all the information you might need to access during the execution of your resolver or pipeline.

The most relevant keys that you'll probably need are:

  • ctx.args: A map that contains all GraphQL request arguments.

  • ctx.stash: A stash is an object that lives throughout the whole execution of the resolver or pipeline. It can be used to pass information between request and response functions from a single resolver or between different resolvers in the same pipeline.

  • ctx.result and ctx.error: These values will contain the result or errors from this resolver. One could for example access these keys from the response function to access the response provided by the Data Source.

  • ctx.prev.result: Contains the value of the previous operation result. F.e.: the result of the response function from the previously executed resolver in a pipeline.

Not everything is allowed

And Appsyncs Safe word is "The code contains one or more errors". This is an error that you have probably already faced if you started developing JS resolvers like you were developing Lambdas.

JS Resolvers run in a custom runtime called APPSYNC_JS and is pretty limited regarding the operations that you will be able to perform inside the resolvers. To help developers avoid this kind of error, AWS provides an ESLint plugin that can be used to statically analyze your code to quickly find problems

Some of the non-allowed operations that you probably wanted to use are:

  • async and await: These operations are not allowed, the only async/await operation that is allowed is the communication with the configured data source, which will be handled by AppSync.

  • try - catch: You won't be able to use try-catch blocks, so make sure your code is top-notch and thoroughly tested.

  • import and third-party dependencies: JS resolvers can't import or depend on any third-party dependency a part of @aws-appsync/utils which is already built into the custom runtime.

These are just a subset of the non-allowed operations to see the rest of them refer to the official documentation.

@aws-appsync/utils, your new best friend

This library will be the only one that you'll be able to import into your code, but don't worry about it, it contains most of the helper functions that you might need for your business logic.

The most relevant ones that you'll probably end up using are:

  • util.error and util.appendError: helpers used to handle errors that you might want to throw/append to the response. util.error will throw an exception and break the execution and util.appendError will append the error and allow for the execution to continue.

  • runtime.earlyReturn: This helper will break the execution of the current resolver and its input will be returned as the response. For example, calling runtime.earlyReturn({}); in the request function will avoid the response function from being executed, but it won't break the execution of the pipeline.

  • util.dynamodb.toDynamoDB and util.dynamodb.toMapValues: helpers that convert input objects to the appropriate DynamoDB representation.

  • util.time.*: A set of helper functions to ease the handling and creation of date times.

To see all of the provided operations you can check the official documentation.

Size Matters

Some people might say size doesn't matter but in this context, for JS resolvers, we have some size limits that we need to follow:

  • 4,000 characters: This is the maximum size currently allowed for resolver code that is defined in-line in the cloud formation template. This will be your current limit if you're using the serverless-appsync-plugin but don't worry, there is a fix for that coming soon.

  • 32,000 characters: In case use another IaC approach to the plugin and you're uploading the resolver code to S3, there is a higher allowed limit where you can use up to 32,000 characters per resolver.

Environment Variables, where are you?

For developers used to Lambdas, they might miss using Environment variables, especially if they change depending on the stage you deploy to.

AppSync/JS Resolvers don't currently support the use of environment variables but with the use of the serverless-appsync-plugin, you might take advantage of a feature called variable substitutions.

To use them you will just need to add a variable in your code with the following structure:

const tableName = '#myTable#';
return {
  operation: "BatchGetItem",
  tables: {
    [tableName]: { keys },
  },
};

The value assigned to the variables must be wrapped with # and the value in between should be the exact name used in your serverless.yml file, otherwise, it won't be updated during the deployment.

After setting the variable in your resolvers code, you will just need to add the substitution configuration on the serverless.yml file:

appSync:
  name: my-api
  substitutions: #global substitutions
    myTableName: !Ref myTable

  resolvers:
    Query.user:
      dataSource: my-table
      substitutions: #resolver substitutions
        myTableName: !Ref myTable

Refactoring is possible, you just need one extra step

As Benoît Bouré explained in his article a few months ago, it's possible to write reusable/refactor code and still use it as a JS resolver.

To take advantage of it you will need to consider a few things:

  • AppSync only supports ES modules

  • We will need to bundle the code before referencing it in our IaC

  • We don't want to bundle the AppSync utils library

To bundle the code we would recommend you use the following command:

esbuild \
  --bundle \
  --target=esnext \
  --platform=node \
  --format=esm \
  --external:@aws-appsync/utils \
  --tree-shaking=true \
  --minify-whitespace \
  --minify-identifiers \
  --outdir=path/to/resolvers/build \
  path/to/resolvers/*.js

The previous command will bundle your code to the expected format to be referenced in your IaC.

We don't recommend using the --minify option since we found that the underlying --minify-syntax tends to introduce some unsupported operations. That's why we use --minify-whitespace and --minify-identifiers sepparately.

To automate this step, I would recommend you add the execution of the bundling as a new step to the deployment pipeline and always reference the resolvers from the build folder in your IaC.

A PR to remove this extra step is currently in the making, make sure to keep an eye on the plugins changelog to see if this step is still needed as of the time you're reading this.

No more headaches, use GraphBolt

Most of you will already be familiarized with Postman or any other tool that allows you to execute API requests and I know switching tools might be time-consuming, but you can think of GraphBolt as the Postman on steroids or the perfect AppSync companion.

The most relevant features that will help you save a lot of time and headaches are:

  • GraphQL Client: A client that allows you to execute requests. Like Postman but tailored for AppSync, allowing for almost-automatic authentication handling, autocomplete and query formatting.

  • Query Inspector: This allows you to go one step ahead of Postman but without the need of leaving the app. You will be able to: see all the recently executed queries, see the details of a single query (including the X-Ray traces, Cloudwatch logs and the executed resolvers) and even debug the execution of each resolver by accessing the Resolver details. This last option will allow you to see how every request/response function has been evaluated.

  • Resolver Evaluation: As mentioned before, not everything is allowed in JS resolvers, and "The code contains one or more errors" is not useful at all. To streamline your development and find the issues easier you can use this function to test and evaluate the resolver code your building.

Conclusion

Getting started with AppSync and JS resolvers might seem like a daunting task, but by giving a quick read to the official documentation and having all the above-mentioned points in mind, you'll be up and running in no time developing AppSync GraphQL APIs.

Did you find this article valuable?

Support Lorenzo Hidalgo Gadea by becoming a sponsor. Any amount is appreciated!