This is V2 of this post, updated with new schemas and the most up to date directives from Amplify.
Cover 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 8 directives offered by the GraphQL Transform library:
@model
// Deploys DynamodDB table + 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 data access patterns for @model types
@versioned
// Adds object versioning and conflict resolution to a type
@http
// Allows you to quickly configure HTTP resolvers within your AWS AppSync API
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
- Chat App
- E-Commerce App
- WhatsApp Clone
- Reddit Clone
- Multi-user 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
# Follow the steps to give the project a name, environment name, and set the default text editor.
# Accept defaults for everything else and choose your AWS Profile.
# If the app needs auth, add auth (choose the defaults)
$ amplify add auth
$ amplify add api
> Choose GraphQL
> If using authentication, choose Amazon Cognito as one of the authentication types
> 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
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 only Admins to be able to create, update, and delete events and public or other signed in users (non-Admins) 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. There is also an optional Comment
type that could be implemented to allow comments on an event and a one to many relationship enabled between events and comments using the @connection
directive.
Based on these requirements, the user should be able to:
- List events in order by date of event
- View an individual event
- Create a comment (signed in users only)
- View comments on an 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: "itemType", fields: ["itemType", "time"], queryField: "eventsByDate")
@auth(rules: [
{ allow: groups, groups: ["Admin"] },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
name: String!
description: String
time: String!
itemType: String!
comments: [Comment] @connection #optional comments field
}
# Optional Comment type
type Comment @model
@auth(rules: [
{ allow: owner, ownerField: "author" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
message: String!
author: String
}
Creating an event
When creating a new event, we would need to populate the itemType
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"
itemType: "Event"
}) {
id name description time
}
}
Note: because
itemType
will 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(itemType: "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.
Creating a comment on an event
Using the eventCommentsId
field, you can specify the event that you would like to associate with the comment:
mutation createComment {
createComment(input: {
eventCommentsId: "7f0d82f5-b57e-4417-b515-ce04475675a2"
message:"Amazing!"
}) {
id
message
}
}
Chat app
In this example we'll look at how to create a basic chat app. For a more real-world chat-app that manages multiple users with authorization, check out the Multi-user Chat App example.
This app has the following requirements. The user should be able to:
- Create a conversation
- Create a message in a conversation
- View all conversations and messages
- Subscribe to new messages and conversations in real-time
type Conversation @model {
id: ID!
name: String
messages: [Message] @connection(keyName: "messagesByConversationId", fields: ["id"])
createdAt: String
updatedAt: String
}
type Message
@model(subscriptions: null, queries: null)
@key(name: "messagesByConversationId", fields: ["conversationId"]) {
id: ID!
conversationId: ID!
content: String!
conversation: Conversation @connection(fields: ["conversationId"])
createdAt: String
}
The following GraphQL queries and mutations would allow users to create a conversation, create a message in a conversation, and view all conversations and messages:
mutation createConversation {
createConversation(input: {
name: "my first conversation"
}) {
name
id
}
}
mutation createMessage {
createMessage(input: {
conversationId: "your-conversation-id"
content: "Hello world"
}) {
id
content
}
}
query listConversations {
listConversations {
items {
name
messages {
items {
content
}
}
}
}
}
E-commerce App
This app has the following requirements. The Customer should be able to:
- Create an account
- View products
- Create an order
- Add line items to an order
- View their account and all associated orders and items
An admin should be able to:
- Create, update, and delete products, orders and customers
- Fetch orders, products, and customers
- Fetch orders by customer id
Based on these requirements we can assume we need the following for this application:
- Product, Customer, Line Item, and Order types
- Database tables for Products, Customers, Line Items, and Orders
- 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
orders: [Order] @connection(keyName: "byCustomerId", fields: ["id"])
}
type Product @model(subscriptions: null)
@auth(rules: [
{ allow: groups, groups: ["Admin"] },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
name: String!
description: String
price: Float!
image: String
}
type LineItem @model(subscriptions: null)
@key(name: "byOrderId", fields: ["orderId"])
@auth(rules: [
{ allow: owner },
{ allow: groups, groups: ["Admin"] }
]) {
id: ID!
orderId: ID!
productId: ID!
qty: Int
order: Order @connection(fields: ["orderId"])
product: Product @connection(fields: ["productId"])
description: String
price: Float
total: Float
}
type Order @model(subscriptions: null)
@key(name: "byCustomerId", fields: ["customerId", "createdAt"], queryField: "ordersByCustomerId")
@auth(rules: [
{ allow: owner },
{ allow: groups, groups: ["Admin"] }
]) {
id: ID!
customerId: ID!
total: Float
subtotal: Float
tax: Float
createdAt: String!
customer: Customer @connection(fields: ["customerId"])
lineItems: [LineItem] @connection(keyName: "byOrderId", fields: ["id"])
}
You should be able to now perform the following GraphQL operations against the API:
mutation createProduct {
createProduct(input: {
name: "Yeezys"
description: "Best shoes ever"
price: 200.00
}) {
id
name
description
price
}
}
mutation createCustomer {
createCustomer(input: {
name: "John Doe"
email: "johndoe@myemail.com"
address: "555 Hwy 88"
}) {
id
email
name
address
}
}
mutation createOrder {
createOrder(input: {
subtotal: 250.00
total: 275.00
tax: 25.00
customerId: "some-customer-id"
}) {
id
subtotal
tax
total
customer {
name
}
}
}
mutation createLineItem {
createLineItem(input: {
qty: 1
productId: "some-product-id"
orderId: "some-order-id"
price: 250.00
total: 250.00
}) {
id
qty
}
}
query getCustomer {
getCustomer(id: "some-customer-id") {
id
name
address
orders {
items {
id
lineItems {
items {
description
price
total
qty
product {
id
name
description
}
}
}
}
}
}
}
query ordersByCustomerId {
ordersByCustomerId(
customerId: "some-customer-id"
) {
items {
id
lineItems {
items {
id
price
total
}
}
total
subtotal
tax
}
}
}
query listOrders {
listOrders {
items {
id
total
subtotal
tax
lineItems {
items {
id
price
product {
id
price
description
}
}
}
}
}
}
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 tables for Users, Conversations, and Messages
- GraphQL definition for mutations (create, update, delete users, conversations, and messages)
- GraphQL definition for queries
- 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
@key(fields: ["userId"])
@model(subscriptions: null)
@auth(rules: [
{ allow: owner, ownerField: "userId" }
]) {
userId: ID!
avatar: String
conversations: [ConvoLink] @connection(keyName: "conversationsByUserId", fields: ["userId"])
messages: [Message] @connection(keyName: "messagesByUserId", fields: ["userId"])
createdAt: String
updatedAt: String
}
type Conversation
@model(subscriptions: null)
@auth(rules: [{ allow: owner, ownerField: "members" }]) {
id: ID!
messages: [Message] @connection(keyName: "messagesByConversationId", fields: ["id"])
associated: [ConvoLink] @connection(keyName: "convoLinksByConversationId", fields: ["id"])
members: [String!]!
createdAt: String
updatedAt: String
}
type Message
@key(name: "messagesByConversationId", fields: ["conversationId"])
@key(name: "messagesByUserId", fields: ["userId"])
@model(subscriptions: null, queries: null) {
id: ID!
userId: ID!
conversationId: ID!
author: User @connection(fields: ["userId"])
content: String!
image: String
conversation: Conversation @connection(fields: ["conversationId"])
createdAt: String
updatedAt: String
}
type ConvoLink
@key(name: "convoLinksByConversationId", fields: ["conversationId"])
@key(name: "conversationsByUserId", fields: ["userId"])
@model(
mutations: { create: "createConvoLink", update: "updateConvoLink" }
queries: null
subscriptions: null
) {
id: ID!
userId: ID!
conversationId: ID!
user: User @connection(fields: ["userId"])
conversation: Conversation @connection(fields: ["conversationId"])
createdAt: String
updatedAt: String
}
type Subscription {
onCreateConvoLink(userId: ID): ConvoLink
@aws_subscribe(mutations: ["createConvoLink"])
onCreateMessage(conversationId: ID): Message
@aws_subscribe(mutations: ["createMessage"])
}
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(subscriptions: null)
@key(fields: ["userId"])
@auth(rules: [
{ allow: owner, ownerField: "userId" }
]) {
userId: ID!
posts: [Post] @connection(keyName: "postByUser", fields: ["userId"])
createdAt: String
updatedAt: String
}
type Post @model
@key(name: "postByUser", fields: ["authorId", "createdAt"])
@auth(rules: [
{ allow: owner, ownerField: "authorId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
authorId: ID!
author: User @connection(fields: ["authorId"])
postContent: String
postImage: String
comments: [Comment] @connection(keyName: "commentsByPostId", fields: ["id"])
votes: [PostVote] @connection(keyName: "votesByPostId", fields: ["id"])
createdAt: String
voteCount: Int
}
type Comment @model
@key(name: "commentsByPostId", fields: ["postId"])
@auth(rules: [
{ allow: owner, ownerField: "authorId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
authorId: ID!
postId: ID!
text: String!
author: User @connection(fields: ["authorId"])
votes: [CommentVote] @connection(keyName: "votesByCommentId", fields: ["id"])
post: Post @connection(fields: ["postId"])
voteCount: Int
}
type PostVote @model
@auth(rules: [
{ allow: owner, ownerField: "userId"},
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
@key(name: "votesByPostId", fields: ["postId"]) {
id: ID!
postId: ID!
userId: ID!
post: Post @connection(fields: ["postId"])
createdAt: String!
vote: VoteType
}
type CommentVote @model
@auth(rules: [
{ allow: owner, ownerField: "userId"},
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
@key(name: "votesByCommentId", fields: ["commentId"]) {
id: ID!
userId: ID!
commentId: ID!
comment: Comment @connection(fields: ["commentId"])
createdAt: String!
vote: VoteType
}
input VoteInput {
type: VoteType!
id: ID!
}
enum VoteType {
up
down
}
This implementation will prevent users from voting multiple times, but you must implement a custom resolver to set the vote ID as a combination of the postId
and the user's userId
.
The most secure way to do this would be on the server, by reading the user's userId
from their JWT. To do so, you could first use these two lines of code to set the vote ID to be the unique combination of the itemId
and the user's ID:
#set($itemId = "$context.identity.username#$context.args.postId")
$util.qr($context.args.input.put("id", $util.defaultIfNull($ctx.args.input.id, $itemId)))
Then, delete or comment out the conditional expression code that does not allow the vote to be overridden:
#set( $condition = {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id"
}
} )
Vote aggregation
If you would like to implement either a vote count total or some type of algorithmic vote count of your own choosing, you can enable a DynamoDB trigger to invoke a Lambda function where you can write your own custom logic.
Here is an example of a DynamoDB trigger that implements an atomic counter, similar to what you would probably want to do in a situation where you need to be sure votes are incremented or decremented consistently.
Multi-user Chat App
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
@key(fields: ["userId"])
@model(subscriptions: null)
@auth(rules: [
{ allow: owner, ownerField: "userId" }
]) {
userId: ID!
conversations: [ConvoLink] @connection(keyName: "conversationsByUserId", fields: ["userId"])
messages: [Message] @connection(keyName: "messagesByUserId", fields: ["userId"])
createdAt: String
updatedAt: String
}
type Conversation
@model(subscriptions: null)
@auth(rules: [{ allow: owner, ownerField: "members" }]) {
id: ID!
messages: [Message] @connection(keyName: "messagesByConversationId", fields: ["id"])
associated: [ConvoLink] @connection(keyName: "convoLinksByConversationId", fields: ["id"])
members: [String!]!
createdAt: String
updatedAt: String
}
type Message
@key(name: "messagesByConversationId", fields: ["conversationId"])
@key(name: "messagesByUserId", fields: ["userId"])
@model(subscriptions: null, queries: null) {
id: ID!
userId: ID!
conversationId: ID!
author: User @connection(fields: ["userId"])
content: String!
conversation: Conversation @connection(fields: ["conversationId"])
createdAt: String
updatedAt: String
}
type ConvoLink
@key(name: "convoLinksByConversationId", fields: ["conversationId"])
@key(name: "conversationsByUserId", fields: ["userId"])
@model(
mutations: { create: "createConvoLink", update: "updateConvoLink" }
queries: null
subscriptions: null
) {
id: ID!
userId: ID!
conversationId: ID!
user: User @connection(fields: ["userId"])
conversation: Conversation @connection(fields: ["conversationId"])
createdAt: String
updatedAt: String
}
type Subscription {
onCreateConvoLink(userId: ID): ConvoLink
@aws_subscribe(mutations: ["createConvoLink"])
onCreateMessage(conversationId: 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(subscriptions: null)
@key(fields: ["userId"])
@auth(rules: [
{ allow: owner, ownerField: "userId" },
{ allow: private, operations: [read] }
]) {
userId: ID!
posts: [Post] @connection(keyName: "postsByUserId", fields: ["userId"])
createdAt: String
updatedAt: String
following: [Following] @connection(keyName: "followingByUserId", fields: ["userId"])
}
type Post @model
@key(name: "postsByUserId", fields: ["authorId"])
@auth(rules: [
{ allow: owner ownerField: "authorId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
authorId: ID!
content: String!
postImage: String
author: User @connection(fields: ["authorId"])
comments: [Comment] @connection(keyName: "commentsByPostId", fields: ["id"])
likes: [PostLike] @connection(keyName: "postLikesByPostId", fields: ["id"])
}
type Comment @model
@key(name: "commentsByPostId", fields: ["postId"])
@auth(rules: [
{ allow: owner, ownerField: "authorId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
id: ID!
postId: ID!
authorId: ID!
text: String!
likes: [CommentLike] @connection(keyName: "commentLikesByCommentId", fields: ["id"])
author: User @connection(fields: ["authorId"])
post: Post @connection(fields: ["postId"])
}
type PostLike @model
@auth(rules: [
{ allow: owner, ownerField: "userId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
@key(name: "postLikesByPostId", fields: ["postId"])
@key(name: "postLikesByUser", fields: ["userId", "createdAt"], queryField: "likesByUser") {
id: ID!
postId: ID!
userId: ID!
user: User @connection(fields: ["userId"])
post: Post @connection(fields: ["postId"])
createdAt: String!
}
type CommentLike @model
@auth(rules: [
{ allow: owner, ownerField: "userId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
@key(name: "commentLikesByCommentId", fields: ["commentId"])
@key(name: "commentLikesByUser", fields: ["userId", "createdAt"], queryField: "likesByUser") {
id: ID!
userId: ID!
postId: ID!
commentId: ID!
user: User @connection(fields: ["userId"])
post: Post @connection(fields: ["postId"])
createdAt: String!
}
type Following @model
@auth(rules: [
{ allow: owner, ownerField: "followerId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
@key(name: "followingByUserId", fields: ["followerId"]) {
id: ID
followerId: ID!
followingId: ID!
follower: User @connection(fields: ["followerId"])
following: User @connection(fields: ["followingId"])
createdAt: String!
}
Likes
Similarly to the Reddit clone, we need to have some custom logic in our resolver to handle likes. To see how this works, check out the custom resolver in the Reddit Clone
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"] },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
]) {
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(keyName: "commentsByTalkId", fields: ["id"])
}
type Comment @model
@key(name: "commentsByTalkId", fields: ["talkId"])
@auth(rules: [
{ allow: owner, ownerField: "authorId" },
{ allow: public, operations: [read] },
{ allow: private, operations: [read] }
])
{
id: ID!
talkId: ID!
talk: Talk @connection(fields: ["talkId"])
message: String
createdAt: String
authorId: ID!
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.