AWS AppSync Velocity Templates Guide

Gerard Sans
Mar 28 · 6 min read

Complete guide with ready to use examples

Geometric Shapes / 180927 by Sasj

This post is the first in a series of articles covering Velocity Templates. Velocity Templates are used by AWS AppSync to implement GraphQL APIs. We will cover the most common use cases and provide examples so you can use them as quick references.

There are few areas you need to master in order to take the most out of your Velocity Templates. We will cover:

  • Introduction to Velocity Templates (this article)
  • Common Recipes: input validation, setting default values, formatting, shaping and aggregating data, error handling, authorisation (coming)
  • Debugging and troubleshooting (coming)
  • Adding CRUD operations for your types in DynamoDB (coming)
  • Connecting an external HTTP endpoint (coming)
  • Connecting a Lambda Function (coming)
  • Adding in-App searches with Elastic Search (coming)
  • Best Practices while authoring your own templates (coming)

Please let me know if you would like to see any other sections or examples at @gerardsans.

What is AWS AppSync?

AppSync Resolvers

A GraphQL Resolver is a function that provides the output for a specific field in a query which will be part of the response for that query. Learn more about resolvers here

A Resolver in AppSync is composed by a:

  • Data Source. There a few options available including DynamoDB, RDB (Aurora/MySQL), HTTP, Lambda, Elastic Search or even no data source.
  • Request Template. This is where you can change the default behaviour to add authorisation or input validation.
  • Response Template. This is where you can add custom error handling or result transformations. Output will be part of the JSON response back to the client.

Resolvers can access single (Unit) or multiple (Pipeline) data sources. We are going to use the first type, which is the default.

Velocity Templates

Velocity Templates are written in VTL (Velocity Template Language). Using VTL we can dynamically change the Resolver behaviour depending on the Request Context. Some use cases include integrating data sources, adding custom authorisation, validations or implementing error handling.

The Request Context contains information about the GraphQL query, the caller identity and the request HTTP headers. We will cover specific use cases later on this series. For a comprehensive list check this reference.

VTL is a fully featured language supporting statements like conditionals, loops, maps and lists. We will introduce these features progressively in our examples. If you are eager though, read the VTL User Guide.

Hello World Example

type Query {
  helloWorld: String

We attached a Resolver to the helloWorld field and set it to no data source. Let’s follow the diagram below from a Client query, triggering each template, to the final response.

AppSync Resolver (no data source)

Imagine a client runs a query like the following (step 1):

query ExampleQuery {

AppSync will find the field helloWorld and execute its Resolver. We defined this Request Template (step 2)

#set( $var = "world" )
    "version": "2018–05–29",    ## behaviour for null and errors
    "payload": "hello ${var}"   ## $var in formal notation

We used a directive #set to assign the string"world" to $var. The rest represents a JSON object with two keys:

  • Version. You can use 2017–02–28 or 2018–05–29 (recommended). The later allows defining custom behaviours for handling empty results and errors. More details.
  • Payload. When using no data source its content will be passed through to $context.result.

In this template, we used a variable to set the value during execution and render it where we needed it. Notice how we used slightly different notations, with and without curly braces. We will discuss the differences in Best Practices.

VTL directives are not rendered to the output but may render results. Eg: #set, #if, #else, #end, #foreach

Once executed, it will become the following output (step 3). Notice how all VTL including comments are gone.

    "version": "2018–05–29",
    "payload": "Hello world"

As we set no data source for our resolver payload will pass to the next step as $context.result in the Response Template (step 4)

#return( $context.result )

Finally, the result, "Hello world" is returned and placed as the value of the helloWorld field and sent to the client.

  "data" : {
    "helloWorld": "Hello world"

Great! We have covered a lot of ground by going through this example. You can review the whole flow below.

Overview AppSync Resolver (no data source)

Passing data between templates

Aliases allow us to repeat the same field multiple times in our queries. Learn more about aliases here.

query ExampleQuery {
  alias: helloWorld

This should run the Resolver twice as AppSync parses the query. Note that each result will be placed at the corresponding location in the response.

Tip: save some keystrokes using $ctx instead of $context

Now, let’s change our Request Template to:

$util.qr($ctx.stash.put("t0", $util.time.nowEpochMilliSeconds()))
    "version": "2018–05–29",
    "payload": "Hello world"

VTL offers many utilities to deal with time operations (see reference). We will use nowEpochMilliSeconds, to create a timestamp and use it to calculate the time passed between rendering our two templates. To achieve that, we added an entry to the stash map. You use stash to pass data over the next step, in this example, to the Response Template (below).

#set( $t1 = $util.time.nowEpochMilliSeconds() )
#set( $total = $t1 - $ctx.stash.t0 )
#return( "${ctx.result} [${total} ms] [$ctx.stash.t0..${t1}]" )

The Response Template is generating another timestamp and calculating the difference as total. For demonstration purposes, I’ve also added both timestamps to see what happens and when.

 "data": {
  "helloWorld": "Hello world [7 ms] [1553389693379..1553389693386]",
  "alias": "Hello world [20 ms] [1553389693382..1553389693402]"

That’s interesting! Let’s run a similar query with 10 aliases:

  "data": {
    "a0": "Hello world [26 ms] [1553389576785..1553389576811]",
    "a1": "Hello world [41 ms] [1553389576788..1553389576829]",
    "a2": "Hello world [47 ms] [1553389576802..1553389576849]",
    "a3": "Hello world [59 ms] [1553389576805..1553389576864]",
    "a4": "Hello world [81 ms] [1553389576808..1553389576889]",
    "a5": "Hello world [25 ms] [1553389576785..1553389576810]",
    "a6": "Hello world [41 ms] [1553389576788..1553389576829]",
    "a7": "Hello world [41 ms] [1553389576802..1553389576843]",
    "a8": "Hello world [52 ms] [1553389576805..1553389576857]",
    "a9": "Hello world [75 ms] [1553389576808..1553389576883]",
    "a10": "Hello world [3 ms] [1553389576785..1553389576788]"

From these results, we can see that resolvers are executed in parallel in clusters of 5. The total time between the earliest and the latest time was 104ms. Which is much better than it would have been executed in sequence ~500ms.

AppSync Resolver using stash (simplified)


Thanks for reading

Looking for speakers? Look no more

Ping any of us! Nader (@dabit3), Kurt (@kurtiskemple), Dennis Hills (@dmennis) or myself (@gerardsans) will be happy to check availability and dates for you. Let’s be in touch!

Nader Dabit (#1), Kurt Kemple (#2), Dennis Hills (#3), Gerard Sans (#4)

Gerard Sans

Written by

Developer Advocate @AWSCloud | Just be AWSome | MC Speaker Trainer Community Leader | Views are my own | @fullstackcon @ReactiveConf @ngcruise @UphillConf UK ☂