AWS AppSync: Implementing the Simple Web Service - Serverless Pattern 🏗
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.
🙉 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.
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.
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.
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:
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:
- beforePipelineTemplate — used for any pre-function steps you might have
- validateFunction — Validates our input
- createFunction — adds our entity to the database
- 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:
🚀 Code
All the code from the above snippets can be found here:
🙏 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.
📚 Additional Resources:
AWS AppSync : https://aws.amazon.com/appsync/product-details/
GraphQL: https://graphql.org/learn/
AWS WAF integration: https://aws.amazon.com/about-aws/whats-new/2020/10/aws-appsync-adds-support-for-aws-waf/
AWS Lambda Authorisers: https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/
Another good comparison GraphQL vs REST: https://www.altexsoft.com/blog/engineering/graphql-core-features-architecture-pros-and-cons/
📣 Getting in touch!
Happy building!… 🚀
Thank you for reading, Robert.
Reach me on linkedIn here: