How To Add A Public And Private GraphQL API with AWS Amplify

In this lesson we cover how to add a GraphQL API with AWS Amplify consisting of both public and private endpoints.

Published
Aug 20, 20
Duration
13m 24s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

Series List

  1. Setup Amplify & Nuxt

  2. Adding authentication (Part 1, Setup)

  3. Adding authentication (Part 2, Implementation)

  4. Adding a public and private API (Part 1, Setup) - You are here

  5. Adding a public and private API (Part 2, Implementation)

  6. Deploy via S3

Repository


We left off in the last lesson with basic authentication working within our Nuxt + Amplify application. Our users could login, register, confirm their email, and logout. Today we'll be expanding our application by adding a private and public access GraphQL API using the Amplify CLI.

The Amplify documentation covers this topic pretty well, so be sure to at least glance over the documentation incase you should run into any issues.

Adding & Defining Our API

So, just like we did with authentication, we need to use the Amplify CLI to add an api to our project. To get started let's run $ amplify add api in our terminal. Once we do, the Amplify CLI will walk us through the following configuration process and allow us to define our GraphQL schema.

  1. Please select from one of the below mentioned services: GraphQL

  2. Provide API name: amplifynuxt

  3. Choose the default authorization type for the API: Amazon Cognito User Pool
    Using User Pools will give us the ability to use the @auth directive to define access controls on our endpoints for authenticated users.

  4. Do you want to configure advanced settings for the GraphQL API: Yes, I want to make some additional changes.

  5. Configure additional auth types?: Yes

  6. Choose the additional authorization types you want to configure for the API: API key
    Cognito User Pool can't define access controls for unauthenticated users, so in order to allow public read access to our API, we need this second authorization type of API key.

  7. Enter a description for the API key: public access

  8. After how many days from now the API key should expire (1-365): 365
    After 365 days we'll need to regenerate a new API key for our application to maintain read access.

  9. Configure conflict decection?: No

  10. Do you have an annotated GraphQL schema?: No

  11. Do you want a guided schema creation?: Yes

  12. What best describes your project?: One-to-many relationship

  13. Do you want to edit the schema now?: Yes

Example Amplify Schema

Amplify will now pop the example GraphQL schema open in your default text editor. If the file opened is blank or Amplify doesn't open the file for you it can be found at:

amplify/backend/api/[api name]/schema.graphql

Once you have the example GraphQL schema open you should see something like this:

type Blog @model {
  id: ID!
  name: String!
  posts: [Post] @connection(name: "BlogPosts")
}
type Post @model {
  id: ID!
  title: String!
  blog: Blog @connection(name: "BlogPosts")
  comments: [Comment] @connection(name: "PostComments")
}
type Comment @model {
  id: ID!
  content: String
  post: Post @connection(name: "PostComments")
}

As you can see we have blog, post, and comment types. Each of these contain a model directive (@model). This model directive states that each one of these types will a table within our database.

Inside each type we then have a list of fields along with their types. So, for example, the first field in our Blog model is an id of type ID. The exclamation point trailing the type denotes the field as required and not nullable.

We also have relationships defined via the connection directive (@connection). Each connection directive contains a name argument. It's important to note here that each end of the relationship receives the same name on it's connection directive. So posts on type Blog has the same connection name as blog on type Post.

Defining Our Schema

Okay, so now that we've briefly covered what's going on with the example schema let's begin defining our own. Let's start by emptying this file out.

Our schema will contain two different models, User and Post. Instead of focusing on scale, we'll mostly be focusing on access controls. Before we begin with access controls, though, we'll begin by getting the models and fields defined.

Let's start by defining our User model.

type User @model {
  id: ID!
  email: String!
  createdAt: String!
  name: String
  biography: String
  website: String
  posts: [Post] @connection(name: "UserPosts")
}

So far nothing here should look too different from the Amplify example. One oddity that might've entered your head is the String type on our createdAt field. GraphQL doesn't come with a native type for dates or datetimes, so we'll be using a string to hold a timestamp value.

Let's move forward with our Post model.

type Post @model {
  id: ID!
  title: String!
  summary: String!
  body: String!
  createdAt: String!
  authorId: String!
  author: User! @connection(name: "UserPosts")
}

Now, since we're storing our user's id as authorId and not userId, we need a way to tell Amplify how to pull the user data for our author field. To do this we'll extend our connection directive with a second argument of keyField. Amplify will then pull the user data for our author field using the id stored in the field we define as the keyField value.

So, in our case, we'll want to define keyField as authorId.

author: User! @connection(name: "UserPosts", keyField: "authorId")

Now, it's important to note that the keyField needs to be defined on both ends of the relationship. So, we also need to do the same for our connection directive on our User model.

posts: [Post] @connection(name: "UserPosts")

Defining Our Access Controls

Now that we have the base schema defined, we can move into defining our access controls. To do this we'll be using the auth directive (@auth). The auth directive accepts an array of rules, each rule being an object. So, to start out, let's define the auth directive on our User model.

type User
  @model
  @auth(
    rules: []
  ) {
  id: ID!
  email: String!
  createdAt: String!
  name: String
  biography: String
  website: String
  posts: [Post] @connection(name: "UserPosts", keyField: "authorId")
}

Owner Access

Next let's start by adding a rule to allow owners to update and delete records that they own. We can also extend this rule to allow creating. This will result in allowing authenticated users as a whole to create since you can't own a record before it's created.

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

Now, by default, Amplify will add an owner field to our model if we don't explicitly define an owner field or a way to track owners. However, for our User model, we'll be using the same id value as the currently authenticated user at the time the record is created. This is how we'll bind our Cognito user to our database user, they'll share ids. Thanks to this, our owner can be defined as the record's id value.

To do this, we can use the ownerField property to define which field in our table to pull the owner's id off of, which is id in our case. So, let's add this to our rule.

@auth(
  rules: [
    { allow: owner, ownerField: "id", operations: [create, update, delete] }
  ]
)

Admin Access

Next up, let's give admins the ability to do whatever they want. We'll be doing this using groups, which you can create and assign groups to users within AppSync.

@auth(
  rules: [
    { allow: owner, ownerField: "id", operations: [create, update, delete] }
    { allow: groups, groups: ["admin"], operations: [create, update, delete] }
  ]
)

Now, any user we assign the group "admin" to within AppSync will be able to create, update, and delete any user record within our User model.

Authenticated User Read Access

If a user doesn't match either of the two criteria thus far, they currently wouldn't be able to do anything with our API. Therefore, we need to continue by allowing authenticated user's the ability to read any record within our User model.

@auth(
  rules: [
    { allow: owner, ownerField: "id", operations: [create, update, delete] }
    { allow: groups, groups: ["admin"], operations: [create, update, delete]
    { allow: private, operations: [read] }
  ]
)

Private here refers to authenticated users. So, we're stating any authenticated user can read from the User model.

Unauthenticated User Read Access

Since we don't want to require our users to login in order to be able to view our content, we'll also want to allow unauthenticated users to have read access.

@auth(
  rules: [
    { allow: owner, ownerField: "id", operations: [create, update, delete] }
    { allow: groups, groups: ["admin"], operations: [create, update, delete] }
    { allow: private, operations: [read] }
    { allow: public, operations: [read] }
  ]
)

Similar to how private is our authenticated users, public is our unauthenticated users.

Authorization Types

Whenever it comes time to specify which authorization type our API call should use our owner, groups, and private rules all deal with authenticated users. So, these are all okay to use the Cognito User Pool authorization type.

However, since Cognito User Pool only handles authenticated user access controls, our public access will need to use the secondary API key authorization type we set up.

Applying Rules To Post

That's it for our User model. We're now ready to move onto our Post model access controls and, for the most part, this will be the same.

type Post 
  @model 
  @auth(
    rules: [
      { allow: owner, ownerField: "authorId", operations: [create, update, delete] }
      { allow: groups, groups: ["admin"], operations: [create, update, delete] }
      { allow: private, operations: [read] }
      { allow: public, operations: [read] }
    ]
  ) {
  id: ID!
  title: String!
  summary: String!
  body: String!
  createdAt: String!
  authorId: String!
  author: User! @connection(name: "UserPosts")
}

The only difference with our Post model authorization rules is our ownerField. Since the post id isn't going to be our owner's id, it'll be a uniquely generated id, we can't use that as the ownerField. However, we are explicitly defining our authorId which, in this case, will be the same as our owner's user id. Therefore, we can use our authorId as the ownerField.

Schema Review

Awesome, we're now done defining our schema and our API access controls. Let's take a quick moment here to confirm what I have and what you have match so we're on the same page.

type User
  @model
  @auth(
    rules: []
  ) {
  id: ID!
  email: String!
  createdAt: String!
  name: String
  biography: String
  website: String
  posts: [Post] @connection(name: "UserPosts", keyField: "authorId")
}

type Post 
  @model 
  @auth(
    rules: [
      { allow: owner, ownerField: "authorId", operations: [create, update, delete] }
      { allow: groups, groups: ["admin"], operations: [create, update, delete] }
      { allow: private, operations: [read] }
      { allow: public, operations: [read] }
    ]
  ) {
  id: ID!
  title: String!
  summary: String!
  body: String!
  createdAt: String!
  authorId: String!
  author: User! @connection(name: "UserPosts")
}

If it's a match, give the file a save and return back to your terminal.

Finishing Our API Setup

Now that we're done defining our schema we can return back to our terminal and hit "enter." Amplify will then validate our schema, and if all is okay we should be finished with the add API flow.

Next, let's run $ amplify push to push our API configuration and schema up. Amplify will then use our configuration and schema to create the necessary resources and generate our API.

When you run amplify push, it should show you a preview of what changed, showing that we've created an API. Go ahead and hit "enter" to continue.

It will then ask us a couple of questions about generating some pre-defined queries, mutations, and subscriptions.

  1. Do you want to generate code for your newly created GraphQL API?: Yes

  2. Choose the code generation language target: javascript

  3. Enter the file name pattern of graphql queries, mutations, and subscriptions: src/graphql/**/*.js

  4. Do you want to generate/update all possible GraphQL operations - queries, mutations, and subscriptions: Yes

  5. Enter maximum statement depth: 2

Amplify will then spend a few minutes creating all the necessary resources for our API. You can take a quick look at the newly generated queries, mutations, and subscriptions that are within your project at /src/graphql. We'll use these in the next lesson to communicate with our backend. It's also worth noting that we don't need to use these queries, mutations, or subscriptions. We could create our own if we wish and use them the exact same way we'll be using these in the next lesson.

Wrapping Up

So we've now successfully created our GraphQL API with both public and private access where only owners and admins will be able to update and delete records. In the next lesson, we'll put our API to work and create some Vuex modules to handle the communication.

Join The Discussion! (1 Comments)

Please sign in or sign up for free to join in on the dicussion.

  1. Commented 1 year ago

    I don`t really understand how this User model conects to Cognito user pool. In my current project I have to show specific data ( models ) to specific user ( cognito user ). But user don`t create this data and can`t update or delete. How to achive this?

    0

    Please sign in or sign up for free to reply