AppSync: Basically GraphQL as a service

raoulmeyer

Raoul Meyer

Posted on February 2, 2019

AppSync: Basically GraphQL as a service

Last year I learned about a new addition to the AWS lineup when I went to a conference. It's called AppSync, and it's basically a way to host a GraphQL service in a serverless way. Last month I experimented with it a bit and in this post I want to share my findings.

TLDR: If you're already using or want to use GraphQL, then you should definitely give AppSync a shot!

What does AppSync do for you?

You only need two things to get started: a schema and some resolvers. The schema you upload is a regular GraphQL schema. AppSync will accept queries and mutations based on this schema. You can change this schema at any time and you don't even need to deploy your new version, it will immediately start using it.

The second part you need are resolvers. These resolvers will map fields from a query to the actual data for that field, which most of the times means fetching data from some datastore. You can hook up Lambda's as resolvers, but you can also use a range of different databases as data sources.

A basic example

To get a grip on how this works, here's a small example. Imagine a website just like this one. There are users identified by some id, and these users have posts. The following schema combines these types, and a query to fetch the data for a user:

type Query {
    user(id: Int): User
}

type User {
    id: Int!
    name: String!
    posts(count: Int): [Post]
}

type Post {
    id: Int!
    title: String!
    content: String!
}
Enter fullscreen mode Exit fullscreen mode

To resolve a User or Post, we'll be running the following lambda resolvers:

exports.user = async (event, context) => {
  const body = await fetchUser(event.arguments.id);

  return {
    statusCode: 200,
    body: JSON.stringify(body)
  };
};

exports.posts = async (event, context) => {
  const { id, count } = event.arguments;
  const body = await fetchPostsForUser(id, count);

  return {
    statusCode: 200,
    body: JSON.stringify(body)
  };
};
Enter fullscreen mode Exit fullscreen mode

From this code we create two lambda's, which we then need to hook up to the fields in our schema. To do that, we need to specify a request and response mapping.

The request mapping will transform data that has been supplied in the query, for example the user id, to the event variable that is available in our lambda.The response mapping will transform the response we give in our lambda back to the structure we defined in the schema.

For the posts resolver, that could look like this:

Request mapping:

{
    "version" : "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "arguments": $util.toJson({
            "id": $context.source.id,
            "count": $context.arguments.count
        })
    }
}
Enter fullscreen mode Exit fullscreen mode
Response mapping:

#if($ctx.result.statusCode == 200)
    $ctx.result.body
#elseif($ctx.result.statusCode == 404)
    $util.error("Not found")
#else
    $util.error("Error")
#end
Enter fullscreen mode Exit fullscreen mode

You can put quite some magic in these mappings. The following request mapping will query DynamoDB directly instead of using a lambda:

Request mapping:

{
    "version" : "2017-02-28",
    "operation" : "GetItem",
    "key" : {
        "id" : {
            "S" : "${context.arguments.id}"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you want to fiddle around with some examples, AWS provides sample setups, which include everything you need to get started.

Why should you use it?

There are a couple of reasons why I would recommend trying AppSync.

Because it's cheap

Just like other serverless services, you pay for what you use and nothing more. There is no base cost for having the service up and running. That means that for applications that have a small user base, this is a very cost effective way of running a GraphQL server. Currently, you pay 4 dollars for every million requests you do, which is comparable to API Gateway pricing and only about 5 times more expensive than the cheapest AWS Lambda function you could have.

Because it will handle any load

It doesn't matter if it's Black Friday, your services will still be up and running. There are no knobs to tweak here. It's really similar to Lambda in that aspect, you just give it your GraphQL schema and it runs it for you.

Because it's really easy to couple to DynamoDB or Lambda

Currently, AppSync offers resolvers for DynamoDB, RDS, Elasticsearch, Lambda and HTTP. For each of these, you'll need to map the data from the GraphQL query to a format that makes sense for the data source. There's clear documentation available for each of these data sources, making it easy to get started with this. The nice thing is that for the most popular databases, you won't have to create a lambda to only run a query.

Because it supports real-time subscriptions

AppSync comes with a number of client libraries, which support real-time subscriptions. With this, clients will keep their connection to AppSync open and are able to subscribe and receive updates on mutations that happened. I can see that being really powerful in certain use cases, you can read read more about it here.

But are there downsides?

Performance

While trying this out, it became really clear that there's quite some overhead in the AppSync internals. When hooked up to a single lambda which just returned a value, the response times rarely got below 150 milliseconds. In most cases, that's probably an acceptable number. It does seem like a lot compared to when you run a GraphQL server yourself. Even compared to API Gateway, which provides a similar service in a lot of ways, it's significantly slower.

Limitations

AppSync is still quite new, becoming generally available in April last year. Because of that, it still has some rough edges. The biggest rough edge to me right now is that the RDS resolver only works for Aurora serverless. Luckily, you can still write your own lambda to get data from any type of RDS of course.

Also good to know is that there are limitations regarding concurrency. When you want to resolve a list of things of the same type, it will run at most 5 concurrent queries/lambda's/HTTP calls. Depending on your use case, you may or may not run into this limit.

Coupling with data sources is a bit nasty

I like to use Cloudformation to deploy my stacks. When you want to deploy an AppSync stack with Cloudformation, all the resolvers need to be defined in your stack template as well. This includes any queries you want to execute, and any business logic you want to apply before and after running those queries.

Having the queries and logic right next to the stack definition is not ideal. It makes it almost impossible to test the logic you define. Also, deploying changes in your business logic becomes a stack change. This means that stuff like rollbacks and canary releases are not trivial anymore.

Even if you're not using Cloudformation, you'll need to specify mappings between your schema and resolvers. AWS chose to use Apache Velocity Template Language for this purpose. Unfortunately, the logic you add there will be hard to test and hard to debug. It's not a dealbreaker though, and it's probably something that's going to be improved as the product ages.

Conclusion

AppSync is a really useful addition to the AWS product lineup. It has some rough edges, but it serves a clear and increasingly popular purpose. If you're planning to build something with GraphQL, I'd recommend giving it a try.

💖 💪 🙅 🚩
raoulmeyer
Raoul Meyer

Posted on February 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related