DEV Community

Cover image for A guide for build  CRUD App with Amplify and AWS AppSync
kris
kris

Posted on • Originally published at instamobile.io

A guide for build CRUD App with Amplify and AWS AppSync

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

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@authdirective 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 typein 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 typeas an object coming from the JavaScript background. For example, in the above schema for the Todomodel is the type that has three fields: id, nameand 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.

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: idand 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

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 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.

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.

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 namefield 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.

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

Next, import TextInputand TouchableOpacityfrom React Native to create an input field and pressable button with some custom styling defined in StyleSheetreference object below the Homecomponent. 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 startcommand 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.

/* 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 addTodocustom 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 todosarray. It takes nameas 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 listTodosinside 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 fetchTodosto fetch the data by running the query listTodos. It is going to be an asynchronous method, so let's use the try/catchblock 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:

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 loadingwith an initial value of boolean falsein the Homecomponent. 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 todosarray the mutation deleteTodoneeds to be executed. Let's add a button on the UI using a TouchableOpacityand@expo/vector-iconsto 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 removeTodothat will delete the todo item from the todosarray as well as update the array by using the filtermethod on it. The inputfor 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.

Originally published at https://www.instamobile.io on October 5, 2020.

Top comments (0)