Everything You Should Know About the AppSync JavaScript Pipeline Resolvers

Everything You Should Know About the AppSync JavaScript Pipeline Resolvers

In November 2022, AWS announced JavaScript support for AppSync Resolvers. It was a long-awaited feature that everyone was excited about.

In this article, I will try to shed some light on this new feature, what JS resolvers are, how they can improve developer experience, what possibilities they unlock; but also their limitations.

What JS Resolvers Are Not

Before we start, it is important to make some clarification about what JS resolvers are not. There might be some misconceptions that will arise, mostly because they allow writing resolvers in a way that many developers already write their Lambda functions with (i.e. JavaScript). We should clear that out of the way from the beginning: JS resolvers are not a replacement for Lambda function resolvers, they will never be. You won't be able to do whatever you want; they come with a set of limitations that we will explore later. They are, however, a good alternative to Lambda in some cases and should simplify the choice between both options.

What Are JS Resolvers, Then?

They are a new way for AppSync to communicate with your data sources. This is what we have always known as “mapping templates”; except that instead of writing them in the VTL language, you can now use (a subset of) JavaScript, which brings tons of benefits (see below). You still need to attach your JS resolvers to a data source, and the only thing they should do is generate a request for that data source and format the response it returns.

From the doc:

The request handler takes a context object as an argument and returns a request payload in the form of a JSON object used to call your data source. The response handler receives a payload back from the data source with the result of the executed request

What benefits do they bring?

Now that we made clear what JS resolvers are, let's talk about their benefits; because there are! (other than "I don't like VTL 🤓")

Better Developer Exprience

Being able to write AppSync handlers in JavaScript obviously makes it easier for everyone. JS is a language that most developers are familiar with, and it has so many benefits over VTL such as better IDE support or code linting. If you want type support, you can also write them in TypeScript as long as you transpile them to JavaScript later.

Room for extension

JavaScript makes it possible to write reusable code by creating custom functions, utilities, or libraries. This was not (easily) possible with VTL. This should make it easier to write better code faster.

No cold start and no extra cost

JS resolvers, unlike Lambda functions, will not suffer cold start, and there is no penalty for choosing JS over VTL in terms of performance. They are also available at no extra cost. Being able to write resolver handlers as JS will potentially allow many to get rid of unnecessary Lamba functions; reducing latency and cost at the same time.

Limitations

If JS resolvers have some benefits, you should be aware of their limitations. These limitations might evolve over time, but some of them will likely never go away. Keep in mind that if JS resolvers don't allow you to do some things, it might as well be to protect you from using bad practices.

Only Pipeline resolvers are supported

For the time being, only Pipeline resolvers are supported. Unit resolvers still require the usage of VTL mapping templates.

Hint: 💡 If you still want to use JS with a unit resolver, you can always write a pipeline resolver that has only one function.

Unsupported JS features

It is primordial to remember that JS resolvers use a subset of ECMAScript 6. It means that all features are not available. For example, you can't use: for loops (for-in and for-of are supported), try, catch, finally, continue, do-while or while loops.

throw is also not supported. If you want to "throw" an error, you should use the util.error() util, which is basically the equivalent to the $ctx.util.error() that you already know.

Arrow functions are not supported either.

No Async/Await

async/await are not supported, for obvious reasons. JS resolvers should be completely synchronous and return as fast as possible.

No network access

You won't be able to do any network request in a JS resolver (e.g. you can't use fetch/axios).

💡 Hint: If you need to do that you probably want to use an HTTP resolver instead.

Size limitation and import

If you follow the above rules, you are free to do everything you want. While you can't use the import statement (except with @aws-appsync/utils), you should be able to include custom libraries and code if you bundle them in a single file before you upload them to AppSync. (e.g. using esbuild).

There is a big catch though: all the code in the bundle MUST be a valid ES6 module, follow all the above AppSync JS rules, and the final package must not exceed 32KB.

Unfortunately, this means that you will likely never be able to include almost any npm package out there at the moment. But, it opens the door to creating dedicated AppSync-compatible libraries that strictly follow the requirements. A quick POC shows that this is possible.

Please refer to the documentation for a full list of supported and non-supported features.

What Else Has Changed?

The AppSync utils

In VTL, we were used to invoking functions like $util.dynamodb.toDynamoDBJson(). Those utilities still make sense and are useful in JS. They now live in a new npm package: @aws-appsync/utils.

This new package also contains an API for interacting with AppSync: extensions.

CloudWatch logs

Those who, like me, were used to diving into the CloudWatch AppSync logs will notice new log types: BeforeRequestFunctionEvaluation, RequestFunctionEvaluation, ResponseFunctionEvaluation, AfterResponseFunctionEvaluation.

These log records are equivalent to the ones that you used to see when debugging VTL mapping templates (namely BeforeMapping, AfterMapping, RequestMapping, and ResponseMapping), but they are specific to JS resolvers. They look roughly the same as their VTL counterpart.

{
  "logType": "RequestFunctionEvaluation",
  "fieldName": "getPost",
  "resolverArn": "arn:aws:appsync:us-east-1:12345678912:apis/abcdefghijklmnop/types/Query/resolvers/getPost",
  "functionName": "getPost",
  "fieldInError": false,
  "evaluationResult": {
    "operation": "GetItem",
    "key": {
      "id": {
        "S": "b03a2a76-1f2c-4150-9b3c-aab88a04f49e"
      }
    }
  },
  "parentType": "Query",
  "path": [
    "getPost"
  ],
  "requestId": "83e25c50-8d97-4939-8475-440c0ed8b36d",
  "context": {
    "arguments": {
      "id": "b03a2a76-1f2c-4150-9b3c-aab88a04f49e"
    },
    "prev": {
      "result": {}
    },
    "stash": {},
    "outErrors": []
  },
  "errors": [],
  "graphQLAPIId": "ykwu6mw2mrc3ho4jczeg6p42pa",
  "functionArn": "arn:aws:appsync:us-east-1:12345678912:apis/abcdefghijklmnop/functions/uvwyyz"
}

You can also write custom logs by using the console.log() function. You will see them appear in CloudWatch.

83e25c50-8d97-4939-8475-440c0ed8b36d INFO - code.js:5:3: "Hello from JS Resolver!"

Conclusion

JS resolvers will greatly improve the developer experience. They should help developers adopt direct integration with data sources (e.g. DynamoDB) more often, while Lambda functions are still useful for more advanced and complex use cases.

There is still a lot to explore, though. This is only the beginning. I am sure the community will come up with great ways to use them and extend them 🚀

Credits: Cover image generated by dall·e