AWS AppSync: Implementing the Simple Web Service - Serverless Pattern 🏗

Robert Bulmer
AWS Tip
Published in
7 min readMar 23, 2022

--

Serverless Patterns: Simple Web Service

A Simple Web Service Pattern

The popular Simple Web Service pattern in Serverless is typically implemented with a RESTful API Gateway endpoint, a Lambda proxy integration and a NoSQL database.

AWS AppSync

AppSync is a fully managed GraphQL service offered by Amazon Web Services and can be used to build scalable enterprise APIs. A typical AppSync solution consists of small resolvers that can be combined to access various data sources such as databases, HTTP APIs or AWS Lambda.

Today, I’m going to show you how we can design the Simple Web Service pattern using GraphQL with AWS AppSync. We will remove the AWS Lambda compute layer which can help reduce complexity and cost, in some scenarios.

A Simple Web Service Using AppSync

🙉 Why GraphQL and AppSync?

See more information on AppSync and why I recommend considering GraphQL and AppSync here

Why this approach?

As already mentioned one of the main advantages of this approach is reducing the overhead of a compute layer (AWS Lambda in the traditional pattern). Using AppSync’s pipeline resolvers and velocity template language VTL we can eliminate the need for AWS Lambda in simple scenarios.

Latency for AppSync resolver functions is extremely low and compute is free, included, in the AppSync pricing model.

AppSync natively supports direct integration with many AWS services, DynamoDB, RDS, and many more through HTTP Resolvers. You can use this functionality to your advantage and save compute time and cost in your Serverless applications.

Consider needing to start an asynchronous process with a SQS queue or StepFunction workflow. You can trigger these directly from AppSync without the need to spin up an intermediary compute layer.

AppSync example integrations / data sources. Communications with many AWS services

I mentioned them, but what exactly are pipeline Resolvers?

Pipeline resolvers are a collection of related functions that are triggered on a field of a GraphQL schema. At the time of writing, you can have up to 10 functions in one request. Each function can perform basic scripting commands, validation and conditional logic.

Example AppSync pipeline resolver with three functions

In the above, as an example: you could chain a simple input validation step, with a HTTP call to read another API, and finally save a final item to a DynamoDB table. The possibilities are huge and makes pipeline resolvers powerful in many ways.

Let’s have a look at the cost of the AppSync Service:

💵 AWS AppSync costs:

Typical Requests — $4 / 1M Requests & $0.09 / 1GB of data transferred

Caching & real-time (web sockets) — $2.00 per million Real-time Updates & $0.08 per million minutes of connection to the AWS AppSync service. Caching starts at $0.044 per hour

For more information see: https://aws.amazon.com/appsync/pricing/

🔐 AWS AppSync Security:

  • AWS WAF
  • JWT authorization with Custom Lambda Authorisers
  • logging to CloudWatch automatically
  • fine-grained access control via IAM

🚀 Implementation

All example code can be found here: https://github.com/rbulmer55/appsync-simple-web-service

Lets’s start with a simple CDK repository written in Typescript.

We can start by defining our GraphQL schema. Here we can call mutation addSong with artist and name and fetch a list of songs back with query getSongs.

> lib/api/schemas/simple-schema.graphql type Song {
id: String!
name: String!
artist: String!
}
type Query {
getSongs: [Song!]
}
input SongInput {
name: String!
artist: String!
}
type Mutation {
addSong(input: SongInput!): Song
}

Let’s add this schema to a new AppSync API named “simple-web-service-songs”:

> lib/api/simple-web-service.tsexport class SimpleWebServiceStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const api = new appsync.GraphqlApi(this, "Api", {
name: "simple-web-service-songs",
schema: appsync.Schema.fromAsset(
path.join(__dirname, "schemas/simple-schema.graphql")
),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.IAM,
},
},
xrayEnabled: true,
});

Next Step we need to connect our AppSync API to a new DynamoDB table so we can store our list of songs. For AppSync to read/write from the database we need to add a DataSource:

> lib/api/simple-web-service.tsconst simpleWebTable = new dynamodb.Table(this, "SimpleWebTable", {
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING,
},
});
const simpleWebDBDS = api.addDynamoDbDataSource(
"DDBSimpleWebDataSource",
simpleWebTable
);

Next we want to create a validation function for our addSong mutation. This means we’ll have to connect up some VTL functions! I will show you a few ways to do this: fromFile, fromString and using the AWS-CDK AppSync helpers.

In AppSync any function that doesn’t connect to a data source must be set with a none data source. Let’s see how this all works below:

> lib/api/simple-web-service.tsconst simpleWebNoneDS = api.addNoneDataSource("none");const validateFunction = new appsync.AppsyncFunction(
this,
"validateSongFn",
{
name: "simpleWebValidateFunction",
api,
dataSource: simpleWebNoneDS,
//fromFile VTL demo
requestMappingTemplate: appsync.MappingTemplate.fromFile(
path.join(
__dirname,
"resolvers/songs/functions/function.validateSong.req.vtl"
)
),
//fromString VTL demo. functionValidateSongsRes = "{}"
responseMappingTemplate: appsync.MappingTemplate.fromString(
functionValidateSongRes
),
}
);

✅ Validation

Now we have our validate function connected to our function.validateSong.req.vtl file let’s take a look at some quick validation.

But, I thought GraphQL was typed? Why do we have custom validation?

It is, however even with our schema artist: String! you can pass an empty String and GraphQL doesn’t throw an error. String! says that the property must exist on the payload, but doesn’t check the property value.

Default GraphQL validation sometimes is not enough

Let’s say we want artist to be validated to match a regex, and return a nice message if a user provides invalid input:

> lib/api/resolvers/songs/functions/function.validateSong.req.vtl#set($errors = [])#if (!$util.matches("^a-zA-Z{1,100}$", $ctx.args.input.artist))
#if ($util.isNullOrEmpty($ctx.args.input.artist))
$util.qr($errors.add("Artist is a required property"))
#else
$util.qr($errors.add("$ctx.args.input.artist is not a valid artist property"))
#end
#end
#if ($errors.size() > 0)
$util.error($util.toJson($errors))
#end
## Return if no errors
{}

Here we check our artist property, in $ctx.args.input.artist, matches a simple regex a-z characters 1–100 in length. If the users input does not match, we return a useful message depending on whether it is invalid or missing completely. Let’s see this in action:

Missing ‘artist’ property
Invalid ‘artist’ property: No numeric characters allowed

Now we have a quick validation function we can add it to a pipeline resolver and we’re almost ready to rock! 🎸

However, we still need a create song function using the DynamoDB data source we created earlier.

This time, we’ll use the AWS-CDK AppSync library to create our mapping templates for us! MappingTemplate.dynamoDbPutItem() and MappingTemplate.dynamoDbResultItem()

We then need to add everything into a pipeline resolver:

  1. beforePipelineTemplate — used for any pre-function steps you might have
  2. validateFunction — Validates our input
  3. createFunction — adds our entity to the database
  4. afterPipelineTemplate — transform the data, if needed, before GraphQL
const createFunction = new appsync.AppsyncFunction(this, "createSongFn", {
name: "simpleWebCreateFunction",
api,
dataSource: simpleWebDBDS,
/*
* CDK can make mapping templates so much easier!!
*/
requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(
appsync.PrimaryKey.partition("id").auto(),
appsync.Values.projecting("input")
),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});
new appsync.Resolver(this, "createSongPipeline", {
api,
typeName: "Mutation",
fieldName: "addSong",
requestMappingTemplate:
appsync.MappingTemplate.fromString(beforeValidateSong),
pipelineConfig: [validateFunction, createFunction],
responseMappingTemplate:
appsync.MappingTemplate.fromString(afterValidateSong),
});

One last step…. Add the query getSongs to retrieve our entities using the nice CDK helpers again.

simpleWebDBDS.createResolver({
typeName: "Query",
fieldName: "getSongs",
requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(),
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});

😅 Now Time to Test!

Using the GraphQL editor we can execute our queries directly:

Retrieving songs from AWS AppSync and DynamoDB

🙏 Summary

AppSync is fantastic at reducing latency and cost in your Serverless simple web service architecture. Unlike lambdas there will be no cold start times or compute configuration needed.

Introducing velocity templates VTL can be a quicker and more direct way to access information within a database. Additionally using HTTP data sources, you can also forward a message onto so many different AWS services, such as a SQS queue or SNS topic.

In my opinion, another benefit of using AppSync is the functions and mapping configurations give developers a lot more responsibility to own their own applications in the entirety, as the resources are much closer to the code. If you work for an organisation who limit developers access to create resources within AWS, maybe consider a more traditional API Gateway approach.

For more complex integrations or working around the limitations, you will want to revert to a Lambda Resolver. AWS Lambda is a data source AppSync provides out the box. Although this guide was removing the Lambda computer layer, Lambda should be used where appropriate. AppSync limitations include 1000 max loop iterations for For…Each, 30 second response timeout and 1MB data size limit.

I believe the main drawback for many companies wanting to consider AppSync is vendor lock in. Consider several microservices using AppSync, that over time have many custom VTL functions. The logic will be a lot more difficult to port over to another provider or service.

--

--

Architech Insights | Serverless Engineer/Architect UK — Certified SA and DevOps Pro | AWS Community Builder! 🚀