Open GraphQL
GraphQL Recipes - Building APIs with GraphQL Transform
Nader Dabit
Updated on
・12 min read

Photo by Tarn Nguyen on Unsplash
To view the repo only containing the code and instructions for deploying these applications, click here.
In my post Infrastructure as Code in the Era of GraphQL and Full Stack Serverless I showed how you could leverage GraphQL, the AWS Amplify CLI, and the GraphQL Transform library to build, share, and deploy full stack cloud applications.
In this post I've created annotated GraphQL schemas that you can use to deploy popular types of applications.
The GraphQL Transform library allows you to deploy AWS AppSync GraphQL APIs with features like NoSQL databases, authentication, elasticsearch engines, lambda function resolvers, relationships, authorization, and more using GraphQL schema directives.
For example, take the following schema that is utilizing the @model directive:
type Note @model {
id: ID!
name: String!
description: String
}
This schema will deploy the following:
- GraphQL API
- CRUD GraphQL operations for this base type: Create, Read, Update, Delete, and List
- GraphQL subscriptions (triggered by mutation events; create, update, delete)
- DynamoDB NoSQL Database
- GraphQL resolvers mapping the DynamoDB table to the GraphQL CRUD operations
As of this post there are 7 directives offered by the GraphQL Transform library:
@model
// Deploys DynamodDB + resolvers + additional GraphQL schema
@auth
// Allows the definition of auth rules and builds corresponding GraphQL resolvers based on these rules
@connection
// Enables you to specify relationships between `@model` object types
@searchable
// Handles streaming the data of an @model object type to Amazon Elasticsearch Service and configures search resolvers that search that information
@function
// Allows you to quickly & easily configure AWS Lambda resolvers within your AWS AppSync API
@key
// Enables you to configure custom index structures for @model types
@versioned
// Adds object versioning and conflict resolution to a type
Using this library you can deploy the back end for your application using only an annotated GraphQL schema.
In this post I will show example schemas that, when used with the Amplify GraphQL transform library, will build out the backends for many popular types of applications.
- Todo App
- Events App
- E Commerce App
- WhatsApp Clone
- Reddit Clone
- Chat App
- Instagram Clone
- Conference App
For a tutorial showing how to deploy these applications using the GraphQL transform library, check out the documentation here.
Some applications may require additional custom authorization logic for certain subscriptions that you may not want accessible to all users. To learn more, check out the documentation here.
Testing these out
To deploy any of these applications, run the following commands:
Be sure to first install the AWS Amplify CLI
$ amplify init
# if the app needs auth
$ amplify add auth
$ amplify add api
> Choose GraphQL
> If using authentication, choose Amazon Cognito as authentication type
> Update GraphQL schema
# if the app needs storage (i.e. images or video)
$ amplify add storage
$ amplify push
Testing locally
You can now use local mocking to test serverless GraphQL APIs, databases, and serverless functions locally.
$ amplify mock api
Check out this video for a quick overview of local testing:
Todo App
Let's start with something very basic: a Todo app.
This app has the following requirements. The user should be able to:
- List all Todos
- Create, update, and delete Todos
Based on these requirements we can assume we need the following for this application:
- Todo type
- Database
- GraphQL definition for mutations (create, update, delete todos)
- GraphQL definition for queries (listTodos)
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type Todo @model {
id: ID!
name: String!
description: String
}
This will deploy the entire GraphQL API including the DynamoDB NoSQL database, additional GraphQL schema for GraphQL CRUD and List operations, GraphQL subscriptions, and the GraphQL resolvers mapping the schema to the database.
Events App
Next, let's look at how we might create an events app. A point to notice here is that we will need to have a way for admins to be able to create, update, and delete events and guests to only be able to read events. We also want to be able to query and get a sorted list (by date) of the events.
Based on these requirements, the user should be able to:
- List events in order by date of event
- View individual event
An admin should also be able to:
- Create an event
- Update and delete an event
To build this app, we could use the following annotated GraphQL schema:
type Event
@model
@key(name: "queryName", fields: ["queryName", "time"], queryField: "eventsByDate")
@auth(rules: [{allow: groups, groups: ["Admin"], operations: [create, update, delete]}])
{
id: ID!
name: String!
description: String
time: String!
queryName: String!
}
When creating a new event, we would need to populate the queryName parameter with a consistent value in order to be able to sort by time of the event:
mutation createEvent {
createEvent(input: {
name: "Rap battle of the ages"
description: "You don't want to miss this!"
time: "2018-07-13T16:00:00Z"
queryName: "Event"
}) {
id name description time
}
}
Note: because
queryNamewill be the same ("Event") value across all items, you could also update the resolver request mapping template to auto-populate this value and therefore not need to specify it in the mutation.
Now, to query for a list of sorted events, you could use the following query:
query listEvents {
eventsByDate(queryName: "Event") {
items {
id
name
description
time
}
}
}
Once you've created the authentication by running amplify add auth, you can run amplify console auth to add a user to the Admin group or use a Lambda Trigger to do it automatically when certain users sign up.
E-commerce App
This app has the following requirements. The User should be able to:
- Create an account
- View products
- Purchase products
- View order / orders
An admin should be able to:
- Create, update, and delete products, orders, and users
Based on these requirements we can assume we need the following for this application:
- Product, Customer, and Order types
- Database
- GraphQL definition for mutations (create, update, delete products, customers, and orders)
- GraphQL definition for queries (get, list)
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type Customer
@model(subscriptions: null)
@auth(rules: [
{ allow: owner }, { allow: groups, groups: ["Admin"]}
]) {
id: ID!
name: String!
email: String!
address: String
}
type Product @model
@auth(rules: [
{allow: groups, groups: ["Admin"], operations: [create, update, delete]}
]) {
id: ID!
name: String!
description: String
price: Float!
image: S3Object
}
type S3Object {
bucket: String!
region: String!
key: String!
}
type Order @model
@auth(rules: [
{allow: owner}, {allow: groups, groups: ["Admin"]}
]) {
id: ID!
customer: Customer @connection
total: Float!
order: String
}
WhatsApp Clone
This app has the following requirements. The User should be able to:
- Create an account
- Update their profile with their avatar image
- Create a conversation
- Create a message in a conversation
Based on these requirements we can assume we need the following for this application:
- User, Conversation, and Message types
- Database
- GraphQL definition for mutations (create, update, delete users, conversations, and messages)
- GraphQL definition for queries, mutations, and subscriptions
- GraphQL subscriptions for real-time communication
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type User
@model
@auth(rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
]) {
id: ID!
username: String!
avatar: S3Object
conversations: [ConvoLink] @connection(name: "UserLinks")
messages: [Message] @connection(name: "UserMessages", keyField: "authorId")
createdAt: String
updatedAt: String
}
type Conversation
@model(subscriptions: null)
@auth(rules: [{ allow: owner, ownerField: "members" }]) {
id: ID!
messages: [Message] @connection(name: "ConvoMsgs", sortField: "createdAt")
associated: [ConvoLink] @connection(name: "AssociatedLinks")
name: String!
members: [String!]!
createdAt: String
updatedAt: String
}
type Message
@model(subscriptions: null, queries: null)
@auth(rules: [{ allow: owner, ownerField: "authorId" }]) {
id: ID!
author: User @connection(name: "UserMessages", keyField: "authorId")
authorId: String
content: String!
image: S3Object
conversation: Conversation! @connection(name: "ConvoMsgs", sortField: "createdAt")
messageConversationId: ID!
createdAt: String
updatedAt: String
}
type ConvoLink
@model(
mutations: { create: "createConvoLink", update: "updateConvoLink" }
queries: null
subscriptions: null
) {
id: ID!
user: User! @connection(name: "UserLinks")
convoLinkUserId: ID
conversation: Conversation! @connection(name: "AssociatedLinks")
convoLinkConversationId: ID!
createdAt: String
updatedAt: String
}
type Subscription {
onCreateConvoLink(convoLinkUserId: ID!): ConvoLink
@aws_subscribe(mutations: ["createConvoLink"])
onCreateMessage(messageConversationId: ID!): Message
@aws_subscribe(mutations: ["createMessage"])
}
type S3Object {
bucket: String!
region: String!
key: String!
}
Reddit Clone
This app has the following requirements. The User should be able to:
- Create an account
- Create & delete a post (post can be an image or text)
- Comment on a post
- Vote on a post
- Vote on a comment
Based on these requirements we can assume we need the following for this application:
- User, Post, Comment, and Vote types
- Database
- GraphQL definition for mutations (create, update, delete users, posts and comments)
- GraphQL definition for queries
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type User
@model
@auth(rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
]) {
id: ID!
username: String!
posts: [Post] @connection
createdAt: String
updatedAt: String
}
type Post @model @auth(rules: [{allow: owner, operations: [create, update, delete]}]) {
id: ID!
postContent: String
postImage: S3Object
comments: [Comment] @connection
votes: Int
}
type Comment @model @auth(rules: [{allow: owner, operations: [create, update, delete]}]) {
id: ID!
text: String!
author: String!
votes: Int
post: Post @connection
}
type Vote @model
@key(name: "byUser", fields: ["createdBy", "createdAt"], queryField: "votesByUser")
{
id: ID!
postId: ID!
createdBy: ID!
createdAt: String!
vote: VoteType
}
type S3Object {
bucket: String!
region: String!
key: String!
}
input VoteInput {
type: VoteType!
id: ID!
}
enum VoteType {
up
down
}
Voting
For the voting mechanism to work, we need to make sure that the resolver is incrementing or decrementing the votes count directly at the DB level. To do that, you will need to create a new mutation and request mapping template to increment and decrement the vote on an item (comment or post).
First, add the following types to your GraphQL schema:
voteForPost(input: ItemVoteInput): Post
input ItemVoteInput {
type: ItemVoteType!
id: ID!
}
enum ItemVoteType {
up
down
}
Then create a custom resolver that would look something like this.
#set($vote = 1)
#if ($context.arguments.input.type == "down")
#set($vote = -1)
#end
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : { "S" : "${context.arguments.input.id}" }
},
"update" : {
"expression" : "ADD votes :votes",
"expressionValues" : {
":votes" : { "N" : $vote }
}
}
}
Now, when creating a vote, you will need to do two things:
Make sure that each vote made by the user has a unique id. To do that, you can use a combination of the user ID + the id of the item being voted on.
Make sure the user has already not voted the same vote. To do so, you could query by id of the vote (userId + item ID), then if the item is there check to see if the vote is different. If it is, update it. If it's the same, then do nothing. If it doesn't exist, create the vote.
You also have the ability to query all votes from a user.
Chat App
Click here to view AWS AppSync Chat, a completed full-stack version of this app built with React.
This app has the following requirements. The User should be able to:
- Create an account
- Create a conversation
- Create a message in a conversation
- View a list of all conversations
- Have the ability to create a new conversation with another user
Based on these requirements we can assume we need the following for this application:
- User, Conversation, and Messsage types
- Database
- GraphQL definition for mutations (create, update, delete users, conversations and messages)
- GraphQL definition for queries
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type User
@model
@auth(rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
]) {
id: ID!
username: String!
conversations: [ConvoLink] @connection(name: "UserLinks")
messages: [Message] @connection(name: "UserMessages", keyField: "authorId")
createdAt: String
updatedAt: String
}
type Conversation
@model(subscriptions: null)
@auth(rules: [{ allow: owner, ownerField: "members" }]) {
id: ID!
messages: [Message] @connection(name: "ConvoMsgs", sortField: "createdAt")
associated: [ConvoLink] @connection(name: "AssociatedLinks")
name: String!
members: [String!]!
createdAt: String
updatedAt: String
}
type Message
@model(subscriptions: null, queries: null)
@auth(rules: [{ allow: owner, ownerField: "authorId" }]) {
id: ID!
author: User @connection(name: "UserMessages", keyField: "authorId")
authorId: String
content: String!
conversation: Conversation! @connection(name: "ConvoMsgs", sortField: "createdAt")
messageConversationId: ID!
createdAt: String
updatedAt: String
}
type ConvoLink
@model(
mutations: { create: "createConvoLink", update: "updateConvoLink" }
queries: null
subscriptions: null
) {
id: ID!
user: User! @connection(name: "UserLinks")
convoLinkUserId: ID
conversation: Conversation! @connection(name: "AssociatedLinks")
convoLinkConversationId: ID!
createdAt: String
updatedAt: String
}
type Subscription {
onCreateConvoLink(convoLinkUserId: ID!): ConvoLink
@aws_subscribe(mutations: ["createConvoLink"])
onCreateMessage(messageConversationId: ID!): Message
@aws_subscribe(mutations: ["createMessage"])
}
Instagram Clone
This app has the following requirements. The User should be able to:
- Create an account
- Create a post
- Create a comment on a post
- Follow and unfollow a user
- Like a comment or a post
Based on these requirements we can assume we need the following for this application:
- User, Post, Like, Following, and Comment types
- Database
- GraphQL definition for mutations (create, update, delete users, posts, comments, following, and likes)
- GraphQL definition for queries
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type User
@model
@auth(rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
]) {
id: ID!
username: String!
posts: [Post] @connection
createdAt: String
updatedAt: String
}
type Post @model @auth(rules: [
{allow: owner, operations: [create, update, delete]}
]) {
id: ID!
postImage: S3Object!
comments: [Comment] @connection
likes: Int
}
type Comment @model @auth(rules: [{allow: owner, operations: [create, update, delete]}]) {
id: ID!
text: String!
author: String!
likes: Int
post: Post @connection
}
type Like @model
@key(name: "byUser", fields: ["createdBy", "createdAt"], queryField: "likesByUser")
{
id: ID!
postId: ID!
createdBy: ID!
createdAt: String!
liked: Boolean
}
type Following @model @key(name: "followerId", fields: ["followerId", "createdAt"], queryField: "listFollowing") {
id: ID
followerId: ID!
followingId: ID!
createdAt: String!
}
type S3Object {
bucket: String!
region: String!
key: String!
}
Likes
Similarly to the Reddit clone, we need to have some custom logic in our resolver to handle likes. In the case of Instagram, you probably only want a user to like a post a single time. If they tap like again, you may want to allow them to unlike a post.
We'll also probably want to keep a total count of likes for a post and for a comment. Luckily, this isn't that hard to accomplish. With DynamoDB (or any NoSQL database) we can do this directly in a database operation without having to first read from the database.
To implement the like count, we can first create the following GraphQL mutation definition:
likePost(like: Boolean!): Post
#set($like = true)
#if ($context.arguments.like == false")
#set($like = -1)
#end
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : { "S" : "${context.arguments.input.id}" }
},
"update" : {
"expression" : "ADD likes :likes",
"expressionValues" : {
":likes" : { "N" : $like }
}
}
}
Conference App
Click here to view Conference App in a Box, a completed full-stack version of this app built with React Native.
This app has the following requirements. The User should be able to:
- Create an account
- View a list of talks
- View an individual talk
- Create a comment on a talk
- (optional) Report a comment
An Admin should be able to:
- Create, edit, and delete a talk
Based on these requirements we can assume we need the following for this application:
- Talk, Comment, and (optional) Report types
- Database
- GraphQL definition for mutations (create, update, delete talks, comments, and reports)
- GraphQL definition for queries
- GraphQL resolvers for all operations
To build this app, we could use the following annotated GraphQL schema:
type Talk @model
@auth(rules: [{allow: groups, groups: ["Admin"], operations: [create, update, delete]}])
{
id: ID!
name: String!
speakerName: String!
speakerBio: String!
time: String
timeStamp: String
date: String
location: String
summary: String!
twitter: String
github: String
speakerAvatar: String
comments: [Comment] @connection(name: "TalkComments", keyField: "talkId")
}
type Comment @model {
id: ID!
talkId: ID
talk: Talk @connection(sortField: "createdAt", name: "TalkComments", keyField: "talkId")
message: String
createdAt: String
createdBy: String
deviceId: ID
}
type Report @model
@auth(rules: [
{allow: owner, operations: [create, update, delete]},
{allow: groups, groups: ["Admin"]}
])
{
id: ID!
commentId: ID!
comment: String!
talkTitle: String!
deviceId: ID
}
type ModelCommentConnection {
items: [Comment]
nextToken: String
}
type Query {
listCommentsByTalkId(talkId: ID!): ModelCommentConnection
}
type Subscription {
onCreateCommentWithId(talkId: ID!): Comment
@aws_subscribe(mutations: ["createComment"])
}
In this schema, notice we are adding an additional subscription to listen to new comments by ID. This way we can only subscribe to comments for the talk that we are currently viewing.
Conclusion
My Name is Nader Dabit. I am a Developer Advocate at Amazon Web Services working with projects like AWS AppSync and AWS Amplify. I specialize in cross-platform & cloud-enabled application development.



166
20


Wow 🤩great post!
TIL about @key and @function.
One thing that I have always been a little confused about is @auth. What @auth setting makes data publicly readable (like on reddit/instagram), but otherwise protected? Do I always need to register and login to access data?
I see that reddit post does not have @auth at all, does that mean it's completely public, anybody can read and write?
And the reddit comment has
@auth(rules: [{allow: owner, operations: [create, update, delete]}]), why is it different from a post?Thank you for taking time to come up with all of these examples :D
Hey Herman, thanks for pointing this out! The
Posttype should indeed also have auth rules. Theoperations: [create, update, delete]setting will allow anyone to query & view the posts, but only the creator to be able to update and delete.I've updated the post to reflect this change.
Subscriptions is not turned off for this model. Any user can subscribe to onCreateCustomer and collect name, email, and address of all customers. I'm afraid we will start seeing S3 bucket type data leaks from people leaving subscriptions on.
This model also has subscriptions enabled. I dont think you intend for everyone to subscribe to the Report object.
This typically would be behind a separate dashboard only accessible by admins, and real-time updates are useful for this type of dashboard. The subscriptions themselves would typically be behind some custom authorization rules. I've updated the post to mention this in the introduction for those unaware of how this may work.
You can set authorization rules on subscriptions in AppSync, check out docs.aws.amazon.com/appsync/latest... to learn more about them.
Thanks for your feedback.
Yes, you may not want subscriptions enabled here unless you have an admin dashboard of some sort.
If you look at the expanded GraphQL schema that is created by Amplify, you will see all of the operations and subscriptions that are enabled and can modify the base schema as you see fit. For the purposes of this tutorial, I'll update this to have subscriptions disabled for those who may not be aware.
Looking at the expanded schema won't tell you that subscriptions don't respect @auth rules. You would need to carefully read the documentation or understand the generated vtl. Going by published amplify projects, "those who may not be aware" seems to be a large group of people. Making these sample schemas secure would help inform people why subscriptions shouldn't be left on without understanding the consequences.
I've been setting up a project with AWS Amplify and really enjoying the experience - especially the cognito for sign up/in. 🧡
However, I ran into a roadblock with
amplify add apibecause it assumes that you want to start with a new DynamoDB table. In the case of having an existing DynamoDB table that I'd like to use, I haven't been able to find anything that works to make this connection happen. Are there any docs for this scenario?I noticed in the Instagram clone you used both "operations" and "queries" arguments. Is there any reason?
Hey Usman, yes when I originally published this I used
queries: nullto specify some authorization rules. After publishing the post, I decided to refactor to use theoperationsarray because thequeriesrule will be deprecated for authorization rules.