Front-End Web & Mobile

Introducing Direct Lambda Resolvers: AWS AppSync GraphQL APIs without VTL

This article was written by Ed Lima, Sr. Product Manager, AWS

September 14, 2021: Amazon Elasticsearch Service has been renamed to Amazon OpenSearch Service. See details.

AWS AppSync is a managed serverless GraphQL service that simplifies application development by letting you create a flexible API to securely access, manipulate, and combine data from one or more data sources with a single network call. With AppSync, developers can build scalable applications on a range of data sources, including Amazon DynamoDB NoSQL tables, Amazon Aurora Serverless relational databases, Amazon OpenSearch Service (successor to Amazon Elasticsearch Service) clusters, HTTP APIs, and serverless functions powered by AWS Lambda.

GraphQL is a query language for APIs that enables developers to query and manipulate data from multiple data sources and other APIs easily through a flexible runtime, using an intuitive syntax that describes data requirements and interactions with the backend. GraphQL has both an API component to expose and access data as well as a compute or runtime component where developers can customize their own business logic directly at the API layer.

There are three main elements in an AppSync GraphQL API:

  • GraphQL schema – this is where the API or application data is defined and modeled in a GraphQL schema definition language (SDL). The data modeled in the schema tells API consumers what data can be exposed to authorized clients, with automatically generated API documentation.
  • Data sources – this is the component that points AppSync to where the data is stored (DynamoDB, Aurora, Amazon OpenSearch Service, Lambda, HTTP/REST APIs or other AWS services).
  • Resolvers – provide business logic linking or “resolving” types/fields defined in the GraphQL schema with the data in the data sources.

A resolver is a function or method that is responsible for populating the data for a field or operation defined in the GraphQL schema. Resolvers provide the runtime to fulfill GraphQL queries, mutations, or subscriptions with the right data. AppSync leverages VTL or Apache Velocity Templates to provide a lean and fast compute layer to resolve GraphQL queries or fields.

AWS 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. VTL Resolvers are comprised of request and response mapping templates, which contain transformation and execution logic. In addition to the built-in VTL resolvers in AppSync, when using Lambda functions as data sources developers have a third place to define their business logic in the function itself.

 

 

There are several advantages to using VTL templates in AppSync:

  • VTL templates allow you to separate and customize business, transformation, and execution logic at different stages in the API call, either before it reaches the data source for the request or after the data is retrieved in the response. Think of them as lifecycle hooks for the API call itself.
  • VTL Resolvers in AppSync have powerful integrated utilities that allow developers to automatically generate identifiers ($util.autoId), parse ($util.parseJson) or convert JSON ($util.toJson), perform URL/base64 encoding ($util.urlEncode) or decoding ($util.base64Decode), generate and convert timestamps ($util.time.nowISO8601()), convert XML to JSON ($utils.xml), perform authorization checks, validate formatting and conditions, and much more, all directly in the AppSync API layer. There is no need to create your own logic to perform these tasks.
  • As a logical templating language, VTL allows you to set default values for new items, transform and shape data, iterate over lists, maps, and arrays, filter or change responses based on the user identity.
  • With VTL in AppSync developers can configure batching for Lambda or DynamoDB as well as DynamoDB transactions.
  • VTL resolvers are managed internally by AppSync. For simple business logic there’s no need to deploy extra AWS compute services/resources in your account.

However, if you’re not familiar with VTL you need to learn a new language to take full advantage of these benefits, which can potentially delay the implementation of a GraphQL project for your business. While there are powerful toolchains such as the Amplify CLI and the GraphQL Transformer that can automatically generate VTL for AppSync APIs, our customers have told us that sometimes they just want to write their own resolver logic in a language they are familiar with. Other customers told us they prefer to consolidate all the business logic for GraphQL resolvers for a given type or operation in a single place, when there’s no need to have any code before the request takes place or after the data is retrieved from the data source.

In order to address these requests, today we’re launching Direct Lambda Resolvers, which make VTL templates optional when integrating AppSync with Lambda. Direct Lambda Resolvers make it easier to work with Lambda and GraphQL on AWS, giving you more flexibility when developing GraphQL Resolvers on AppSync with Lambda data sources.

 

 

 

When using Lambda data sources with AppSync you can now create resolver logic without VTL with any runtime supported in Lambda, or you can also use your own custom runtime. You can still have VTL when it makes sense. For instance, you can have a request template enabled and a response template disabled or vice-versa. If you do not wish to use VTL at all and consolidate all your resolver business logic in the Lambda function itself, it’s just a matter of making sure that all VTL templates are disabled for the resolver.

When VTL is disabled, AppSync automatically sends the full context object of the API request call directly to Lambda. The context object contains data related to authorization, identity, query arguments, request headers, as well as the info object. Lambda is then able to easily access the GraphQL API call context data in the function event handler. The response from Lambda doesn’t need to be an object in a specific format, it just needs to comply with the scalar type defined in the GraphQL Schema for the specific field or fields Lambda is resolving. If there’s an error thrown by Lambda, AppSync will automatically populate the “errors” object in the GraphQL response and pass any error message sent by Lambda to the client.

As far as the AWS CLI and AWS CloudFormation are concerned, VTL templates are now fully optional with Lambda data sources. For example, the following command to create an AppSync resolver which doesn’t provide VTL templates executes successfully as long as the data source linked to the resolver is a Lambda function:

aws appsync create-resolver –-api-id myapiexample12345678 --typeName Query --fieldName getOrder –-data-source-name LambdaDS

AppSync automatically infers that because VTL templates were not provided they should be disabled for that specific resolver. For all other data sources such as DynamoDB, VTL request and response templates continue to be required. Thus the command above would return an error in the context of non-Lambda resolvers.

 

Pipeline Resolvers

AppSync executes resolvers on a GraphQL field to resolve, mutate, or retrieve the data from data sources. In some cases, applications require executing multiple backend operations to resolve a single GraphQL field. With pipeline resolvers, developers can compose multiple data source operations, orchestrate, and execute them in sequence.

The new resolver mode for Lambda data sources is also fully supported for pipeline resolvers, which means you can create simple backend orchestration directly in AppSync for multiple Lambda functions to be executed in sequence with a single GraphQL call and no need to use VTL. Pipeline Resolvers without VTL for Lambda data sources can take advantage of the prev.result field in the context object of the API call, passed from one function to the next in the pipeline. For complex or customized orchestration logic in your GraphQL API, you can easily invoke AWS Step Functions directly from AppSync.

If we take the article published a couple of months ago about custom authorization on AppSync with pipeline resolvers as an example, the only VTL logic we actually needed was on the response template for the Authorizer function which takes advantage of VTL utilities to check authorization. Now we can disable all other VTL templates for Lambda data sources in the pipeline and have an AppSync GraphQL pipeline with only the VTL code we need, nothing more nothing less:

 

 

Direct Lambda Resolvers in action: Polyglot Hello World GraphQL API

In order to showcase Direct Lambda Resolvers, we create a simple GraphQL API in AppSync without VTL. The API interacts with four different Lambda functions based on four different runtimes: Go, Node.js, Python, and Ruby.

 

 

Here the very simple code you can use to create the functions in the Lambda console:

 

Go 1.x

package main

import (
        "fmt"
        "context"
        "github.com/aws/aws-lambda-go/lambda"
)

func HandleRequest(ctx context.Context) (string, error) {
        return fmt.Sprintf("hello world from go"), nil
}

func main() {
        lambda.Start(HandleRequest)
}

Node.js 12.x

let response;

exports.lambdaHandler = async (event, context) => {
    try {
        response = {
            node: "hello world from node"
        }  
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

Python 3.7

print('Loading function')

def lambda_handler(event, context):
    hello = "hello world from python"
    return hello

Ruby 2.7

def lambda_handler(event:, context:)
  "hello world from ruby"
end

 

In AppSync we create an API with a simple GraphQL Schema:

schema {
	query: Query
}

type HelloWorld {
	go: String
	node: String
	python: String
	ruby: String
}

type Query {
	getHelloWorld: HelloWorld
}

Then we add the Lambda data sources to the AppSync API following the steps defined in the documentation:

 

 

Next we must link each field in the HelloWorld type to its respective Lambda function. Go to the Schema section of your API in the AppSync console and, under Resolvers, select the appropriate field and click the Attach button:

 

 

In the Create new Resolver page, select the Lambda function according to the runtime from the drop-down menu. For instance, the field go in the HelloWorld type should have the Go Lambda function attached to it. Simply click Save Resolver and it’s all done:

 

 

By default, new resolvers linked to Lambda data sources are configured as Direct Resolvers with VTL disabled. If you want to access, test, modify or work on your Lambda function code, just click the Access Data Source button in the AppSync Resolver settings page. Repeat the resolver creation steps for the Python and Ruby functions.

We also need to attach a resolver for the getHelloWorld query itself. We’ll attach the Node.js function as it returns a JSON object to resolve the node field of the HelloWorld type, all other functions return strings. Once all resolvers are attached you should have something similar to:

 

 

Finally, we query our data in the Queries section of the console to confirm our AppSync API can access all functions:

query {
  getHelloWorld{
    go
    node
    python
    ruby
  }
}

Given the optimized nature of GraphQL we can invoke all four Lambda functions with a single network call and receive the results we defined in the query selection set:

 

 

The AppSync GraphQL backend business logic for our Polyglot Hello World API is defined exclusively based on the four different programming languages we’re using in Lambda, none of them is VTL. If we want to update the existing Lambda resolvers to leverage any of the available VTL utilities (for instance, add a timestamp or generate a uuid identifier), it’s just a matter of editing the resolver and enabling the template of choice.

To celebrate AWS SAM recently becoming generally available, a great milestone for our friends in the SAM team, you can try the Polyglot Hello World GraphQL API in your AWS account using the following SAM template to test AppSync and CloudFormation without VTL. Before building and deploying with the SAM CLI you might need to update the CodeURI parameter for each function accordingly, depending on the folder where you’re storing the code locally. Alternatively, you can clone the functions and the SAM template with the API definition from GitHub:

 

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Polyglot Hello World via GraphQL
Globals:
  Function:
    Timeout: 5

Resources:
  GoHelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: GoFunction/hello-world/
      Handler: hello-world
      Runtime: go1.x
  NodeHelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: NodeFunction/hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
  PythonHelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: PythonFunction/hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
  RubyHelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: RubyFunction/hello_world/
      Handler: app.lambda_handler
      Runtime: ruby2.7
  awsAppSyncServiceRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "appsync.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
  lambdaAccessPolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: "DirectAppSyncLambda"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: "lambda:invokeFunction"
            Resource:
              - !GetAtt GoHelloWorldFunction.Arn
              - !GetAtt NodeHelloWorldFunction.Arn
              - !GetAtt PythonHelloWorldFunction.Arn
              - !GetAtt RubyHelloWorldFunction.Arn
      Roles:
        -
          Ref: "awsAppSyncServiceRole"
  HelloWorldApi:
    Type: "AWS::AppSync::GraphQLApi"
    Properties:
      Name: "PolyglotHellloWorld"
      AuthenticationType: "API_KEY"
  HelloWorldApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
  HelloWorldApiSchema:
    Type: "AWS::AppSync::GraphQLSchema"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      Definition: |
        schema {
          query: Query
        }
        type HelloWorld {
          go: String
          node: String
          python: String
          ruby: String
        }
        type Query {
          getHelloWorld: HelloWorld
        }
  GoDataSource:
    Type: "AWS::AppSync::DataSource"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      Name: "GoDirect"
      Type: "AWS_LAMBDA"
      ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
      LambdaConfig:
        LambdaFunctionArn: !GetAtt GoHelloWorldFunction.Arn
  GoResolver:
    Type: "AWS::AppSync::Resolver"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      TypeName: "HelloWorld"
      FieldName: "go"
      DataSourceName: !GetAtt GoDataSource.Name
  NodeDataSource:
    Type: "AWS::AppSync::DataSource"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      Name: "NodeDirect"
      Type: "AWS_LAMBDA"
      ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
      LambdaConfig:
        LambdaFunctionArn: !GetAtt NodeHelloWorldFunction.Arn
  NodeResolver:
    Type: "AWS::AppSync::Resolver"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      TypeName: "Query"
      FieldName: "getHelloWorld"
      DataSourceName: !GetAtt NodeDataSource.Name
  PythonDataSource:
    Type: "AWS::AppSync::DataSource"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      Name: "PythonDirect"
      Type: "AWS_LAMBDA"
      ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
      LambdaConfig:
        LambdaFunctionArn: !GetAtt PythonHelloWorldFunction.Arn
  PythonResolver:
    Type: "AWS::AppSync::Resolver"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      TypeName: "HelloWorld"
      FieldName: "python"
      DataSourceName: !GetAtt PythonDataSource.Name   
  RubyDataSource:
    Type: "AWS::AppSync::DataSource"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      Name: "RubyDirect"
      Type: "AWS_LAMBDA"
      ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
      LambdaConfig:
        LambdaFunctionArn: !GetAtt RubyHelloWorldFunction.Arn
  RubyResolver:
    Type: "AWS::AppSync::Resolver"
    Properties:
      ApiId: !GetAtt HelloWorldApi.ApiId
      TypeName: "HelloWorld"
      FieldName: "ruby"
      DataSourceName: !GetAtt RubyDataSource.Name   

Outputs:
  HelloWorldAPI:
    Value: !GetAtt HelloWorldApi.Arn
  GoHelloWorldFunction:
    Value: !GetAtt GoHelloWorldFunction.Arn
  NodeHelloWorldFunction:
    Value: !GetAtt NodeHelloWorldFunction.Arn
  PythonHelloWorldFunction:
    Value: !GetAtt PythonHelloWorldFunction.Arn
  RubyHelloWorldFunction:
    Value: !GetAtt RubyHelloWorldFunction.Arn

 

Conclusion

Now you have more flexibility to define where your GraphQL business logic is configured in AWS AppSync. You can leverage the AppSync built-in VTL compute layer for resolvers, have your resolvers run exclusively in Lambda with your preferred runtime, or mix and match using VTL and Direct Lambda Resolvers interchangeably in the same AppSync API.

You can try AppSync and Direct Lambda Resolvers today at https://aws.amazon.com/appsync/, for more information refer to our documentation.

Go build with GraphQL, AWS AppSync, AWS Lambda, and no servers!