AWS Amplify is a framework that lets us develop a web or mobile application quickly. In this tutorial, we are going to continue to learn how to perform CRUD operations with the database by using GraphQL mutations. AWS Amplify has a complete toolchain wiring and managing APIs with GraphQL. The API we will be creating in this tutorial is a GraphQL API using AWS AppSync (a managed GraphQL service) and the database will be Amazon DynamoDB (a NoSQL database).

Please before you begin this tutorial to follow the part 1 where we discussed the setting up Amplify as a framework as well as email authentication with custom UI.

Create a GraphQL API

To start creating an API and connect the database run the below command in a terminal window.

amplify add api

Then select the service GraphQL from the prompt.

This CLI execution automatically does a couple of things. First, it creates a fully functional GraphQL API including data sources, resolvers with basic schema structure for queries, mutations, and subscriptions. It also downloads client-side code and configuration files that are required in order to run these operations by sending requests. The above command will prompt us to choose between what type of API we want to write in. Enter a profile API name. In the below case, we are leaving the profile name to default.

Next, it will again give us two options as to how we want to authenticate the AWS AppSync API. In a real-time application, we have different users accessing the database and making requests to it. Choose the option Amazon Cognito User Pool. This is a more pragmatic approach. Since we have already configured Amazon Cognito User Pool with the authentication module, we do not have to configure it here.

For the next two questions Do you want to configure advanced settings for the GraphQL API and Do you have an annotated GraphQL schema? the answer is N or no. Amplify comes with pre-defined schemas that can be changed later.

When prompted Choose a schema template, select the option Single object with fields.

Before we proceed, let’s edit the GraphQL schema created by this process. Go to the React Native project, and from the root directory, open the file amplify/backed/api/[API_NAME]/schema.graphql.

The default model created by the AppSync is the following:

type Todo @model {
  id: ID!
  name: String!
  description: String
}

Currently, a warning must have prompted when finished the process in the CLI that is described as:

The following types do not have '@auth' enabled. Consider using @auth with @model
   - Todo

Since we have the authentication module already enabled we can add the @auth directive in the schema.graphql file. By default, enabling owner authorization allows any signed-in user to create records.

type Todo @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String!
  description: String
}

If you’re not familiar with GraphQL models and its types here’s a brief overview.

A type in a GraphQL schema is a piece of data that’s stored in the database. Each type can have a different set of fields. Think of the type as an object coming from the JavaScript background. For example, in the above schema for the Todo model is the type that has three fields: id, name and description. Also, the @model directive is used for storing types in Amazon DynamoDB. This is the database used by Amazon when storing our app data.

Every type in a database generates a unique identity to each piece of information stored to further identify and persist in CRUD operations through HTTP requests. In this case, the id is generated by Amplify and has a value of a built-in type of id which in GraphQL terminology, is known as a scalar type.

The exclamation mark ! signifies that the field is required when storing the data and it must have a value. In the above schema, there are two required fields: id and name for the Todo type.

Save this file and all the changes we have just made are now saved locally.

Publish API to AWS Cloud

To publish all the changes we have made (or left as default) in the local environment to AWS Cloud, run the command amplify push from the terminal window.

On running the command, as a prompt, it returns a table with information about resources that we have used and modified or enabled. The name of these resources is described in the Category section.

The Resource name in the above table is the API name chosen in the previous section.

The next column is the type of operation needed to send the API—currently, Create. The provider plugin column signifies that these resources are now being published to the cloud. Press Y to continue.

The Amplify CLI interface will now check for the schema and then compile it for any errors before publishing final changes to the cloud.

In the next step, it prompts us to choose whether we want to generate code for the newly created GraphQL API. Press Y. Then choose JavaScript as the code generation language.

For the third prompt, let the value be default and press enter. This will create a new folder inside the src directory that contains the GraphQL schema, query, mutations, and subscriptions as JavaScript files for the API we have created in the previous section.

Press Y to the next question that asks to update all GraphQL related operations. Also, let maximum statement depth be the default value of 2. It will take a few moments to update the resources on the AWS cloud and will prompt with a success message when done.

At the end of the success message, we get a GraphQL API endpoint. This information is also added to the file aws-exports.js file in the root directory of the React Native project, automatically.

Adding an Input Field in the React Native App

To capture the user input, we are going to use two state variables using React hook useState. The first state variable is for the name name field of the todo item and an array called todos. This array will be used to fetch all the todo items from the GraphQL API and display the items on the UI.

Add the below code before the JSX returned inside the Home component:

const [name, setName] = useState('');
const [todos, setTodos] = useState([]);

Next, import TextInput and TouchableOpacity from React Native to create an input field and pressable button with some custom styling defined in StyleSheet reference object below the Home component. Here’s the complete code for Home.js:

import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Button,
  ScrollView,
  Dimensions
} from 'react-native';
import { Auth } from 'aws-amplify';

const { width } = Dimensions.get('window');

export default function Home({ updateAuthState }) {
  const [name, setName] = useState('');
  const [todos, setTodos] = useState([]);

  async function signOut() {
    try {
      await Auth.signOut();
      updateAuthState('loggedOut');
    } catch (error) {
      console.log('Error signing out: ', error);
    }
  }

  const addTodo = () => {};

  return (
    <View style={styles.container}>
      <Button title="Sign Out" color="tomato" onPress={signOut} />
      <ScrollView>
        <TextInput
          style={styles.input}
          value={name}
          onChangeText={text => setName(text)}
          placeholder="Add a Todo"
        />
        <TouchableOpacity onPress={addTodo} style={styles.buttonContainer}>
          <Text style={styles.buttonText}>Add</Text>
        </TouchableOpacity>
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    marginTop: 20
  },
  input: {
    height: 50,
    borderBottomWidth: 2,
    borderBottomColor: 'tomato',
    marginVertical: 10,
    width: width * 0.8,
    fontSize: 16
  },
  buttonContainer: {
    backgroundColor: 'tomato',
    marginVertical: 10,
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
    width: width * 0.8
  },
  buttonText: {
    color: '#fff',
    fontSize: 24
  }
});

Make sure you are running the expo start command in a terminal window to see the results of this step.

Adding a Mutation using the GraphQL API

A mutation in GraphQL is all about handling operations like adding, deleting, or modifying data. Currently, the React Native application is basic, but it serves the purpose of making you familiar with Amplify as a toolchain and its integration with the cross-platform framework.

To add an item and to retrieve the same in the React Native app, let’s add some business logic to communicate with the GraphQL backend with a mutation.

Inside the file src/graphql/mutation.js there are mutation functions that we can make use of to create, delete, or update a note in the database.

/* eslint-disable */
// this is an auto generated file. This will be overwritten

export const createTodo = /* GraphQL */ `
  mutation CreateTodo(
    $input: CreateTodoInput!
    $condition: ModelTodoConditionInput
  ) {
    createTodo(input: $input, condition: $condition) {
      id
      name
      description
      createdAt
      updatedAt
      owner
    }
  }
`;
export const updateTodo = /* GraphQL */ `
  mutation UpdateTodo(
    $input: UpdateTodoInput!
    $condition: ModelTodoConditionInput
  ) {
    updateTodo(input: $input, condition: $condition) {
      id
      name
      description
      createdAt
      updatedAt
      owner
    }
  }
`;
export const deleteTodo = /* GraphQL */ `
  mutation DeleteTodo(
    $input: DeleteTodoInput!
    $condition: ModelTodoConditionInput
  ) {
    deleteTodo(input: $input, condition: $condition) {
      id
      name
      description
      createdAt
      updatedAt
      owner
    }
  }
`;

This is done by Amplify and to use any of the above mutations, we can directly import the method in the component file. In Home.js file, import API and graphqlOperation from the package aws-amplify. The API is the category for AWS resource and the second function imported is the method to run either a mutation or the query. Also, import the mutation createTodo from graphql/mutation.js file.

// ...
import { Auth, API, graphqlOperation } from 'aws-amplify';

import { createTodo } from '../graphql/mutations';

Let’s add the logic inside the addTodo custom handler method we defined in the previous section. It’s going to be an asynchronous function to fetch the result from the mutation and update the todos array. It takes name as the input where name is the text of the item.

const addTodo = async () => {
  const input = { name };
  const result = await API.graphql(graphqlOperation(createTodo, { input }));

  const newTodo = result.data.createTodo;
  const updatedTodo = [newTodo, ...todos];
  setTodos(updatedTodo);
  setName('');
};

Before we move on to the next section, try adding some data.

Running a Query to Fetch the Data from AWS AppSync

To fetch the data from the database we need to run a query. Similar to mutations, Amplify also takes care of creating initial queries based on GraphQL schema generated.

All the available queries can be found in the src/graphql/queries.js.

/* eslint-disable */
// this is an auto generated file. This will be overwritten

export const getTodo = /* GraphQL */ `
  query GetTodo($id: ID!) {
    getTodo(id: $id) {
      id
      name
      description
      createdAt
      updatedAt
      owner
    }
  }
`;
export const listTodos = /* GraphQL */ `
  query ListTodos(
    $filter: ModelTodoFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        name
        description
        createdAt
        updatedAt
        owner
      }
      nextToken
    }
  }
`;

To fetch all the data from the GraphQL API and display it on the device’s screen, let’s use the query from the file above. Import listTodos inside Home.js file:

import { listTodos } from '../graphql/queries';

To fetch the data from the database, let’s use the useEffect hook. Make sure to import it form React library:

import React, { useState, useEffect } from 'react';

Let’s define another handler method called fetchTodos to fetch the data by running the query listTodos. It is going to be an asynchronous method, so let’s use the try/catch block to catch any initial errors when fetching data. Add the following code snippet inside the Home component:

useEffect(() => {
  fetchTodos();
}, []);

async function fetchTodos() {
  try {
    const todoData = await API.graphql(graphqlOperation(listTodos));
    const todos = todoData.data.listTodos.items;
    console.log(todos);
    setTodos(todos);
  } catch (err) {
    console.log('Error fetching data');
  }
}

The array of data returned from the database looks like the following:

Let’s some JSX to render the data on a device’s screen as well by mapping over the todos array.

return (
  <View style={styles.container}>
    <Button title="Sign Out" color="tomato" onPress={signOut} />
    <ScrollView>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={text => setName(text)}
        placeholder="Add a Todo"
      />
      <TouchableOpacity onPress={addTodo} style={styles.buttonContainer}>
        <Text style={styles.buttonText}>Add</Text>
      </TouchableOpacity>
      {todos.map((todo, index) => (
        <View key={index} style={styles.itemContainer}>
          <Text style={styles.itemName}>{todo.name}</Text>
        </View>
      ))}
    </ScrollView>
  </View>
);

Also, update the corresponding styles:

const styles = StyleSheet.create({
  // ...
  itemContainer: {
    marginTop: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#ddd',
    paddingVertical: 10,
    flexDirection: 'row',
    justifyContent: 'space-between'
  },
  itemName: {
    fontSize: 18
  }
});

Here is the result you are going to get:

Add a Loading Indicator while the Query is Fetching Data

Right now the when the app is refreshed, or when the user logs in, it takes time to make the network call to load the data, and hence, there is a slight delay in rendering list items. Let’s add a loading indicator using ActivityIndicator from React Native.

// modify the following import statement
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Button,
  ScrollView,
  Dimensions,
  ActivityIndicator
} from 'react-native';

To know when to display the loading indicator when the query is running, let’s add a new state variable called loading with an initial value of boolean false in the Home component. When fetching the data, initially this value is going to be true, and only when the data is fetched from the API, its value is again set to false.

export default function Home({ updateAuthState }) {
// ...
const [loading, setLoading] = useState(false);

// modify the fetchTodos method
 async function fetchTodos() {
    try {
      setLoading(true);
      const todoData = await API.graphql(graphqlOperation(listTodos));
      const todos = todoData.data.listTodos.items;
      console.log(todos);
      setTodos(todos);
      setLoading(false);
    } catch (err) {
      setLoading(false);
      console.log('Error fetching data');
    }
  }

  // then modify the JSX contents

  return (
    {/* rest remains same */}
    <ScrollView>
        {/* rest remains same */}
        {loading && (
          <View style={styles.loadingContainer}>
            <ActivityIndicator size="large" color="tomato" />
          </View>
        )}
        {todos.map((todo, index) => (
          <View key={index} style={styles.itemContainer}>
            <Text style={styles.itemName}>{todo.name}</Text>
          </View>
        ))}
      </ScrollView>
  )
}

// also modify the styles

const styles = StyleSheet.create({
  // ...
  loadingContainer: {
    marginVertical: 10
  }
});

Here is the output:

Running the Delete Mutation to Delete an Item

To delete an item from the todos array the mutation deleteTodo needs to be executed. Let’s add a button on the UI using a TouchableOpacity and @expo/vector-icons to each item in the list. In the Home.js component file, start by importing the icon and the mutation.

// ...
import { Feather as Icon } from '@expo/vector-icons';

import { createTodo, deleteTodo } from '../graphql/mutations';

Then, define a handler method called removeTodo that will delete the todo item from the todos array as well as update the array by using the filter method on it. The input for the mutation this time is going to be the id of the todo item.

const removeTodo = async id => {
  try {
    const input = { id };
    const result = await API.graphql(
      graphqlOperation(deleteTodo, {
        input
      })
    );
    const deletedTodoId = result.data.deleteTodo.id;
    const updatedTodo = todos.filter(todo => todo.id !== deletedTodoId);
    setTodos(updatedTodo);
  } catch (err) {
    console.log(err);
  }
};

Now, add the button where the todo list items are being rendered.

{
  todos.map((todo, index) => {
    return (
      <View key={index} style={styles.itemContainer}>
        <Text style={styles.itemName}>{todo.name}</Text>
        <TouchableOpacity onPress={() => removeTodo(todo.id)}>
          <Icon name="trash-2" size={18} color="tomato" />
        </TouchableOpacity>
      </View>
    );
  });
}

Here is the output you are going to get after this step.

Summary

On completing this tutorial you can observe how simple it is to get started to create a GraphQL API with AWS AppSync and Amplify.

At Instamobile, we are building ready to use React Native apps, backed by various backends, such as AWS Amplify or Firebase, in order to help developers make their own mobile apps much more quickly.

Shopping Cart