Amplify Tic-Tac-Toe Tech Exploration
A technical exploration of using AWS' Amplify, AppSync's GraphQL Subscriptions and AppSync's local/none resolver for a tic-tac-toe game.
After my first few technical explorations of Amplify, I want to swerve off the happy-path of most tutorials. Tic-tac-toe is a simple game that has different problems to solve than a simple to-do application. I'll say this here, there are better tech stacks for a tic-tac-toe game.
My plan:
- Use GraphQL's Mutations and Subscriptions as a PubSub communication model.
- Use AppSync's local/none resolvers to remove the need for any database or Lambda functions - it stays all inside AppSync. This makes it more in the PubSub model.
- Try to keep it all in code using the Amplify CLI.
Summary what I learned
- How to create Subscriptions.
- How to create an AppSync local/none resolver that returns the mutation parameters as fields in the subscriptions.
- How to write the resolvers in code within an Amplify project using AWS CloudFormation which I wrote up below.
Take-aways
- Although Amplify attempts to simplify a lot of development, this is a use-case that requires understanding AppSync and using CloudFormation.
- There is a noticable delay between the mutation being sent and the subscription being received. This sometimes caused the game to get into bad states.
How-to setup local resolvers and subscriptions
The following is an overview instead of a detailed step-by-step tutorial.
-
Create a GraphQL API resource with the Amplify CLI.
amplify add api
-
In the GraphQL Schema, create matching mutations and subscriptions. The mutation needs the parameters to match the fields. The subscription parameters are the parameters you want to filter on from the mutation. In this case, we only want to receive messages for the same
gameId
.AppSync requires a Query type which is why there is a noop field for it. amplify/backend/api/tictactoe
type SayHello { gameId: String name: String } type Mutation { sayHello(gameId: String!, name: String!): SayHello } type Subscription { subscribeSayHello(gameId: String!): SayHello @aws_subscribe(mutations: ["sayHello"]) } type Query { # Query is required by Amplify/AppSync noop: SayHello } schema { query: Query mutation: Mutation subscription: Subscription }
If you make the
subscribeSayHello
'sgameId
parameter optional (by removing the!
) and not passing in agameId
when subscribing, you would get thesayHello
events for all games. -
As it stands now, nothing will happen with this API after you
push
it. Next you will return all the mutation parameters as the return fields.The following can also be done via the UI in the AWS Console.
Amplify lets you [provide additional CloudFormation files(aws-amplify/amplify-cli#1002)] at
/amplify/backend/api/<API_NAME>/stacks/CustomResources.json
.Add resources for the None/Local resolver and the request/response configurations for the mutations to the
"Resources"
section ofCustomResources.json
. -
Add the None/Local resolver to
CustomResources.json
.- Give it a
Type
ofAWS::AppSync::DataSource
. - Set
Properties > ApiId
to a reference toAppSyncApiId
which Amplify has defined. - Set
Properties > Type
toNONE
.
{ "Resources": { "NoneDataSource": { "Type": "AWS::AppSync::DataSource", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "Description": "The local (none) resolver data source", "Name": "NoneDataSource", "Type": "NONE" } } } }
- Give it a
-
Add the resolver for the
sayHello
mutation toCustomResources.json
.- Give it a
Type
ofAWS::AppSync::Resolver
. - Set
Properties > ApiId
to a reference toAppSyncApiId
which Amplify has defined. - Set
Properties > DataSourceName
to the Name of NoneDataSource with theGetAtt
function. - Set
Properties > TypeName
toMutation
. This can beQuery
orMutation
. - Set
Properties > FieldName
to thesayHello
. - Set
Properties > RequestMappingTemplateS3Location
to the "Resolver Mapping Template" file in the Velocity Template Language (VTL) format for the request. You'll create this file in the next step. (You can also define it inline withRequestMappingTemplate
) SetProperties > ResponseMappingTemplateS3Location
to the "Resolver Mapping Template" file file for the response. You'll create this file in the next step. (You can also define it inline withResponseMappingTemplate
)
{ "Resources": { "NoneDataSource": {/*...*/}, "SayHelloResolver": { "Type": "AWS::AppSync::Resolver", "Properties": { "ApiId": { "Ref": "AppSyncApiId" }, "DataSourceName": { "Fn::GetAtt": [ "NoneDataSource", "Name" ] }, "TypeName": "Mutation", "FieldName": "sayHello", "RequestMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.sayHello.req.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] }, "ResponseMappingTemplateS3Location": { "Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.sayHello.res.vtl", { "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" }, "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" } } ] } } } }
- Give it a
-
Create the "Resolver Mapping Template" files to return the parameters to the
sayHello
mutation as the fields.Amplify creates an empty folder
resolvers
inside the API you created where you can add custom resolver mapping templates.amplify/backend/api/tictactoe/resolvers/Mutation.sayHello.req.vtl
{ "version": "2017-02-28", "payload": $util.toJson($context.arguments) }
The
payload
value will be passed from the request to the response by AppSync.amplify/backend/api/tictactoe/resolvers/Mutation.sayHello.res.vtl
{ $util.toJson($context.result) }
-
Finally, push the configurations to AWS with
amplify push api
. Now you can subscribe to the mutations to get the values passed in as parameters.