DEV Community

Cover image for GraphQL Recipes (V2) - Building APIs with GraphQL Transform
Nader Dabit for Open GraphQL

Posted on • Updated on

GraphQL Recipes (V2) - Building APIs with GraphQL Transform

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
}
Enter fullscreen mode Exit fullscreen mode

This schema will deploy the following:

  1. GraphQL API
  2. CRUD GraphQL operations for this base type: Create, Read, Update, Delete, and List
  3. GraphQL subscriptions (triggered by mutation events; create, update, delete)
  4. DynamoDB NoSQL Database
  5. 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
Enter fullscreen mode Exit fullscreen mode

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.

  1. Todo App
  2. Events App
  3. Chat App
  4. E-Commerce App
  5. WhatsApp Clone
  6. Reddit Clone
  7. Multi-user Chat App
  8. Instagram Clone
  9. 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
Enter fullscreen mode Exit fullscreen mode

Testing locally

You can now use local mocking to test serverless GraphQL APIs, databases, and serverless functions locally.

$ amplify mock
Enter fullscreen mode Exit fullscreen mode

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:

  1. List all Todos
  2. Create, update, and delete Todos

Based on these requirements we can assume we need the following for this application:

  1. Todo type
  2. Database
  3. GraphQL definition for mutations (create, update, delete todos)
  4. GraphQL definition for queries (listTodos)
  5. 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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. List events in order by date of event
  2. View an individual event
  3. Create a comment (signed in users only)
  4. View comments on an event

An admin should also be able to:

  1. Create an event
  2. 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
}
Enter fullscreen mode Exit fullscreen mode

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
  } 
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create a conversation
  2. Create a message in a conversation
  3. View all conversations and messages
  4. 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
}
Enter fullscreen mode Exit fullscreen mode

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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

E-commerce App

This app has the following requirements. The Customer should be able to:

  1. Create an account
  2. View products
  3. Create an order
  4. Add line items to an order
  5. View their account and all associated orders and items

An admin should be able to:

  1. Create, update, and delete products, orders and customers
  2. Fetch orders, products, and customers
  3. Fetch orders by customer id

Based on these requirements we can assume we need the following for this application:

  1. Product, Customer, Line Item, and Order types
  2. Database tables for Products, Customers, Line Items, and Orders
  3. GraphQL definition for mutations (create, update, delete products, customers, and orders)
  4. GraphQL definition for queries (get, list)
  5. 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"])
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

WhatsApp Clone

This app has the following requirements. The User should be able to:

  1. Create an account
  2. Update their profile with their avatar image
  3. Create a conversation
  4. Create a message in a conversation

Based on these requirements we can assume we need the following for this application:

  1. User, Conversation, and Message types
  2. Database tables for Users, Conversations, and Messages
  3. GraphQL definition for mutations (create, update, delete users, conversations, and messages)
  4. GraphQL definition for queries
  5. GraphQL subscriptions for real-time communication
  6. 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"])
}
Enter fullscreen mode Exit fullscreen mode

Reddit Clone

This app has the following requirements. The User should be able to:

  1. Create an account
  2. Create & delete a post (post can be an image or text)
  3. Comment on a post
  4. Vote on a post
  5. Vote on a comment

Based on these requirements we can assume we need the following for this application:

  1. User, Post, Comment, and Vote types
  2. Database
  3. GraphQL definition for mutations (create, update, delete users, posts and comments)
  4. GraphQL definition for queries
  5. 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
}
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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"
  }
} )
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create an account
  2. Create a conversation
  3. Create a message in a conversation
  4. View a list of all conversations
  5. 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:

  1. User, Conversation, and Messsage types
  2. Database
  3. GraphQL definition for mutations (create, update, delete users, conversations and messages)
  4. GraphQL definition for queries
  5. 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"])
}
Enter fullscreen mode Exit fullscreen mode

Instagram Clone

This app has the following requirements. The User should be able to:

  1. Create an account
  2. Create a post
  3. Create a comment on a post
  4. Follow and unfollow a user
  5. Like a comment or a post

Based on these requirements we can assume we need the following for this application:

  1. User, Post, Like, Following, and Comment types
  2. Database
  3. GraphQL definition for mutations (create, update, delete users, posts, comments, following, and likes)
  4. GraphQL definition for queries
  5. 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!
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create an account
  2. View a list of talks
  3. View an individual talk
  4. Create a comment on a talk
  5. (optional) Report a comment

An Admin should be able to:

  1. Create, edit, and delete a talk

Based on these requirements we can assume we need the following for this application:

  1. Talk, Comment, and (optional) Report types
  2. Database
  3. GraphQL definition for mutations (create, update, delete talks, comments, and reports)
  4. GraphQL definition for queries
  5. 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"])
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (18)

Collapse
 
hermanya profile image
Herman Starikov

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

Collapse
 
dabit3 profile image
Nader Dabit

Hey Herman, thanks for pointing this out! The Post type should indeed also have auth rules. The operations: [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.

Collapse
 
tvthatsme profile image
Timothy Vernon

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 api because 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?

Collapse
 
sakhmedbayev profile image
sakhmedbayev

Hi Nader! Thanks for the post!

I think there is an error in the E-commerce App's schema. This definition:

 @auth(rules: [
    {allow: groups, groups: ["Admin"], operations: [create, update, delete]}
  ])

on Product type will not allow Users to read "2. View products", even "Admin" group will not be able to do that. I think auth transformer should read the following:

@auth(
    rules: [
      { allow: groups, groups: ["Admin"], operations: [read, create, update, delete] }
      { allow: public, operations: [read] }
    ]
  )
Collapse
 
rosswilliams profile image
rosswilliams
type Customer
  @model
  @auth(rules: [
    { allow: owner }, { allow: groups, groups: ["Admin"]}
  ]) {
  id: ID!
  name: String!
  email: String!
  address: String
}
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
rosswilliams profile image
rosswilliams
type Report @model
  @auth(rules: [
    {allow: owner, operations: [create, update, delete]},
    {allow: groups, groups: ["Admin"]}
  ])

This model also has subscriptions enabled. I dont think you intend for everyone to subscribe to the Report object.

Collapse
 
dabit3 profile image
Nader Dabit

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.

Collapse
 
dabit3 profile image
Nader Dabit • Edited

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.

Collapse
 
rosswilliams profile image
rosswilliams

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.

Collapse
 
smaipas profile image
Sotiris Maipas

Thanks for your great post Nader!

I have one question though.
I have the following use-case: I have Users collection and I want to be able to get a user by id or by email.
Using @key is it possible to define two or more different custom indexes or I have to setup a custom resolver?

Collapse
 
dnafication profile image
Dina

Great post Nader,

I was wondering how does the sort by time work in case of the events app since we are declaring the time field as a string and appsync or dynamodb is not told how to sort based on the field.

Collapse
 
johanstn profile image
Johan Steenkamp

Are DynamoDB reserved words allowed in GraphQL types? Sometimes my Amplify build fails with a GraphQL error pointing to a type with reserved name. However this same build would have worked before. DynamoDB reserved words appear in your examples (User) so I'm still not sure what causes the issue.
docs.aws.amazon.com/amazondynamodb...

Collapse
 
dnafication profile image
Dina

Another question around Reddit Clone. Where do I add the custom resolver and what is the naming convention?

Collapse
 
devusman profile image
Usman Suleiman

I noticed in the Instagram clone you used both "operations" and "queries" arguments. Is there any reason?

Collapse
 
dabit3 profile image
Nader Dabit

Hey Usman, yes when I originally published this I used queries: null to specify some authorization rules. After publishing the post, I decided to refactor to use the operations array because the queries rule will be deprecated for authorization rules.

Collapse
 
bwinkers profile image
Brian Winkers

Thanks for updating, the hardest part of learniong Amplify or AppSync development right now is outdated examples.

Collapse
 
davidbiller profile image
David Biller

Mhhhh
i guess in all the schemas, there is no way to check if the user has liked the post already. So we cant show this in the GUI.

Collapse
 
mtopolov profile image
Maxime Topolov

You should try it on code.store a GraphQL back-end as a service we've just released few days ago :)