Part Two
Author's Note: Since this article series was first drafted, AWS announced support for JavaScript resolvers. While I am excited to begin using JS resolvers, the tooling - most notably the serverless-appsync-plugin - has yet to fully support it. Look for new posts in the near future where I will show you how to set up and use JS resolvers. Until then, I think the following content still has value and I hope it helps you.
Your First VTL
In part I, we set up an open HTTP endpoint for getting a product that took a productId
and a storeId
through a path parameter and a header, respectively. We also started an AppSync endpoint that would take an input of productId
and storeId
and return a Product type, all part of our fictional “Big Shopper” app.
Now, let's focus on how we get from AppSync to HTTP. We will use VTL, which is an acronym for Apache's Velocity Template Language. From the AWS programming guide for VTL, "AppSync uses VTL to translate GraphQL requests from clients into a request to your data source. Then it reverses the process to translate the data source response back into a GraphQL response."
The Request
AppSync's request structure can be difficult to make out if you are new to AppSync and/or GraphQL. One quick tip to help here is to create an "echo" Lambda that you can wire in as a data source. This Lambda just echoes, via console.log()
, its event content to CloudWatch (don't worry about the response; just let the call fail).
For example, here is the output from Big Shopper’s getProduct
AppSync call as received by an "echo Lambda". The logs in CloudWatch show us the shape of the input:
As we see, in our event
object, we have a property arguments
which has input
that contains the data we seek: productId
and storeId
. Given this, we are ready to start our VTL for the request.
The first thing we are going to do is capture the particular variables we want to work with. There are ways to shortcut/condense this, but let’s be explicit for now. We will need the productId
so we can insert it in the HTTP route and the storeId
so we can insert it in a header. Let's look at VTL's #set
directive.
The #set
directive allows us to declare and initialize variables, like so:
#set ($storeId = $context.arguments.input.storeId)
Notice that we use the dollar sign to refer to variables, both the one we are creating, $storeId
, and the one that was passed in, $input
. Notice also that the path we are using to find the store id input is the same one we discovered using our echo Lambda, earlier. Repeat this for your productId
on the following line.
#set ($storeId = $context.arguments.input.storeId)
#set ($productId = $context.arguments.input.productId)
Now, we can use our variables to populate the request payload for an HTTP resolver. It has a particular shape1 that we'll follow, covering all the properties that we need on the call. In this case, we need to have version
, method
, resourcePath
, and params.headers
filled out. The following shows how we use our variables as strings:
{
"version": "2018-05-29",
"method": "GET",
"resourcePath": "/products/${productId}",
"params": {
"headers": {
"x-custom-store-id": "${storeId}"
}
}
}
That JSON object we just defined is the "returned" result of the template. That is, we have created the input object for the HTTP resolver in the shape it understands with the data we need to fetch a Product.
Quick Tip: Trailing Commas
Before we go further, beware of one VTL quirk that can/will trip you up: no trailing commas are allowed in your templates. Think JSON, not JavaScript. Most of the errors I encountered while learning VTL were edits that left trailing commas. Your linter probably won't catch them in VTL as it may in a JSON file, so be extra diligent.
The Response
Fortunately for us, the hard part is over. Our response template will be roughly the same for all of our calls, with one exception in the bonus section.
That said, there is one new VTL concept to introduce: the conditional. We want to take the result from the HTTP resolver, and determine if we should hand it back to the caller (success) or raise an error (failure). We are going to handle two types of failures: resolver errors and HTTP errors.
You will encounter what I'm calling "resolver errors" for VTL syntax issues (like trailing commas!) or other issues in your template or data source setup. If you receive a status code other than the one you expected (but otherwise flowed through the resolver correctly), you can raise an error with a different message for your user. Let's look at how we construct this.
The most basic VTL conditional is the if/else
. These directives include a terminator directive, #end
. Let's look at an example.
#if($myDogHasFleas)
$myMap.put("action", "Bath Time")
#else
$myMap.put("action", "Take for Walk")
#end
Here, we evaluate the variable $myDogHasFleas
to ascertain whether we should walk him or bathe him. We could also compare values (#if($fleaCount > 0)
) with any of the standard comparison operators.
Quick Tip: Boolean Evaluation
If you are coming from JavaScript/TypeScript, take note that only a boolean
false
and the valuenull
are considered false in VTL conditionals. Unlike JavaScript, zero and empty string are not consideredfalse
.
Now that we understand basic VTL conditionals, let's create our response template. The first condition we want to check is whether the error is on the GraphQL/AppSync side. We can do this by checking to see if the value of $context.error
is not null and, if so, raise the error using the Utility helper, $util.error()
:
#if($context.error)
$util.error($context.error.message, $context.error.type)
#end
Next comes the check to see if our HTTP response was correct. In our case, we expect our GET call to return a status code of 200. On a 200, return the body of the HTTP call. If we get anything else, raise an error and include the status code:
#if($context.result.statusCode == 200)
$context.result.body
#else
$utils.error("Delivery failed: $context.result.statusCode")
#end
Combining the two, we get our full response mapping template:
#if($ctx.error)
$util.error($ctx.error.message, $ctx.error.type)
#end
#if($ctx.result.statusCode == 200)
$ctx.result.body
#else
$utils.error("Delivery failed: $ctx.result.statusCode")
#end
That's it for simple request and response VTL mappings. In the next post, we'll look at some more complicated HTTP mapping templates and how we can sign our calls to accommodate IAM Authorization.