a CRUD guide with AWS Amplify, AWS AppSync and React Native
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.