Sleemo – A new way to develop serverless GraphQL backend using AWS AppSync

2020.10.29

Introduction

AWS has announced Direct Lambda Resolvers for AWS AppSync in Aug 2020, and I was super excited about this new feature support. Prior to this feature release, VTL(Velocity Template Language) was a MUST to implement GraphQL resolvers using AWS AppSync, which made GraphQL backend implementation using AppSync complicated and hard.

Since the VTL-based control isn't flexible and limited, many developers rather pass GraphQL events and contexts through the VTL resolvers to AWS Lambda as a data source. This had required some extra efforts to get it working, and was not intuitive to some extent. Thanks to this new feature support, there's no need to set an AWS Lambda function as a data source, and no VTL resolvers to maintain. This brought me to think about some way to implement serverless GraphQL backend more efficiently with less code in Python. (Of course, this idea could be applied to other languages, but I guess Python is fair enough to choose because it is one of the favorite languages for developers when using AWS Lambda)

Let's take a look at what the code looks like when you typically implement AWS AppSync resolvers with AWS Lambda.

A Typical Implementation for AppSync Lambda resolvers

Since, we no longer need VTL templates, you can handle GraphQL operations within an AWS Lambda function directly like below:

def handler(event, context):
    # event = {
    #    "arguments": {
    #        "input": {
    #            "username": "twkiiim", 
    #            "title": "Hello Sleemo", 
    #            "content": "A new way to develop serverless GraphQL"
    #        }
    #    },
    #    "info": {
    #        "fieldName": "createPost"
    #    }
    # }
    
    arguments = event['arguments']
    fieldName = event['info']['fieldName']
    
    ...

For clarification, the event argument is generated and passed from AWS AppSync, which originally parsed from the GraphQL mutation below:

mutation CreatePost {
    createPost(input: {
        username: 'twkiiim',
        title: 'Hello Sleemo',
        content: 'A new way to develop serverless GraphQL'
    }) {
        id
        username
        title
        content
    }
}

To handle multiple GraphQL operations in the same Lambda function, which is a usual way to implement, one can typically implement if-else chain for this gateway Lambda function and call some other dedicated function to handle the specific GraphQL operations. A typical implementation for this scenario might look like this:

from createPost import createPost
from getPost import getPost
from updatePost import updatePost
from deletePost import deletePost
from listPost import listPost

def handler(event, context):    
    arguments = event['arguments']
    fieldName = event['info']['fieldName']
    
    if fieldName == 'createPost':
        input = arguments['input']
        return createPost(input)

    elif fieldName == 'getPost':
        input = argument['id']
        return getPost(id)
        
    elif fieldName == 'updatePost':
        input = arguments['input']
        return updatePost(input)
        
    elif fieldName == 'deletePost':
        id = arguments['id']
        return deletePost(id)
        
    elif fieldName == 'listPost':
        next_token = arguments['next_token']
        return listPost(next_token)
    
    else:
        raise Exception('No such GraphQL operation')

Do you see what's the problem here? Even though AWS has announced Direct Lambda Resolvers for AWS AppSync, we still have the tedious tasks like import each function with endless if-else chain. The number of lines of code you have to add is a linear function of the number of GraphQL operations your schema defines, which is typically around 100 or more.

No more words, because everyone would get the point here.

Sleemo - AWS AppSync Direct Lambda Resolver Development Framework

Today, I'm pleased to announce Sleemo - AWS AppSync Direct Lambda Resolver Development Framework. The framework consists of just a few lines of code, but still enough to keep your AWS Lambda resolvers super simple, easy to understand, and let you develop much faster. This is an alpha release, so it has to be updated quite a lot with new features, but I'd like to deliver the conceptual idea behind Sleemo.

Features

  • No need to manually import other resolver functions and manage the tedious if-else based router from the gateway Lambda handler.
  • The arguments of AppSync operations(queries and mutations) being parsed and passed to each resolver functions automatically.
  • Utility functions provided to easily generate AppSync scalar types such as AWSDateTime or AWSTimestamp (will support a lot more!)

Installation

$ pip install sleemo

An example project

An example of Sleemo project could be structured like below:

/
|-- gatewayLambda.py
|-- resolvers/
|----- getTodo.py
|----- listTodo.py
|----- createTodo.py
|----- updateTodo.py
|----- deleteTodo.py
|-- requirements.txt

Note that Sleemo is an AWS AppSync Direct Lambda Resolver Framework, so you typically need to set up a separate deployment project. That means the entire Sleemo project above would be zipped into a single zip file including 3rd-party libraries installed, and the zip file should be uploaded to an AWS Lambda function.

Hence, it's entirely up to you which deployment tool to use. I'm gonna add some guide docs regarding how to deploy Sleemo project with Serverless Framework and AWS CDK as soon as possible.

Back to the example project, let's assume that the GraphQL schema below is defined in the separate deployment project:

type Todo {
  id: ID!
  author: String!
  title: String!
  content: String
  done: Boolean!
  createdAt: AWSDateTime!
}

input CreateTodoInput {
  author: String!
  title: String!
  content: String
  done: Boolean
}

input UpdateTodoInput {
  author: String
  title: String
  content: String
  done: Boolean
}

type Query {
  getTodo(id: ID!): Todo
  listTodo: [Todo!]!
}

type Mutation {
  createTodo(input: CreateTodoInput!): Todo
  updateTodo(input: UpdateTodoInput!): Todo
  deleteTodo(id: ID!): Todo
}

gatewayLambda.py is the default gateway of the AppSync resolver. It receives the event from AWS AppSync directly and route this event to appropriate functions.

from sleemo.framework import get_appsync_framework

sleemo = get_appsync_framework(resolver_path='resolvers')

@sleemo.default_gateway()
def handler(event, context):
    return sleemo.resolve(event)

Do you see the difference? It no longer requires the tedious import statement, no if-else chains. Sleemo handles this tasks internally and all you have to do is pass the resolver_path. resolver_path represents where your resolver functions are located. Take a look at the project structure above and you'll be clear on this.

Now, let's take a look at how each resolver file looks like. Sleemo doesn't care of how each resolver function should be implemented. You can use any libraries you prefer to implement your resolvers. Sleemo just passes the GraphQL operation argument input: CreateTodoInput to createTodo() function with the original event variable.

from sleemo.utils import get_type_utils
from sleemo.framework import get_logger

logger = get_logger()

def createTodo(input, event):

    ## Your business logic here. 
    ## You can use any library in this function, such as pynamodb for DynamoDB ORM.
    ## Below is an example of return data

    logger.info('createTodo start')

    utils = get_type_utils(timezone_offset=9)

    todo = {
        'id': utils.createUUID(),
        'author': input['author'],
        'title': input['title'],
        'content': input['content'],
        'done': False,
        'createdAt': utils.createAWSDateTime(),
    }

    logger.info('createTodo end')

    return todo

Let Me Know How You Think About This

Now you've got the idea of Sleemo, and I'd love to know what you think about this. I'm on Twitter (@twkiiim) or LinkedIn (twkiiim), so please don't hesitate to message me if you have any idea on this.

Thanks for reading this post and hope to see you in another one. Stay tuned! :)