Even though GraphQL has gained a lot of popularity over the past year, building the backend for a GraphQL API can be a major pain and a source of confusion for developers new to the ecosystem.
AWS AppSync allows developers to offload the complexity and time involved with building a GraphQL backend and only worry about building their application, and it does so with real-time and offline capabilities.
In this post, we’ll look at how to create a new AppSync GraphQL API & connect it to a React application. We’ll also add mutations, queries, and subscriptions to the application to make the data real-time.
To view the final code for this app, click here
The application we will be building is a recipe app. Each recipe will have a name, ingredients, and instructions associated with it.
When working with AppSync there are 2 main parts: the client application and the GraphQL API. The client will be our web application, and the API will be the AppSync API hosted on AWS.
We will start by creating the AppSync API, then we’ll create our React application and wire the two together.
To build the API you will need to have an AWS account. If you do not already have one, you can visit the AWS console and sign up.
The first thing we will need to do is open the AppSync console. To do so, go into the AWS console, click Services, and under Mobile Services you will see AWS AppSync. You can also go there directly by visiting https://console.aws.amazon.com/appsync/.
Click on AWS AppSync under Mobile Services, which will take you the AppSync dashboard.
On the top right corner of this dashboard there will be a button that says Create API, click that button to create our new API.
Here, give the API a name (I’m calling mine “Recipes”), and leave the Custom Schema template chosen and click Create.
In the next screen we will be shown the dashboard of our newly created API. We should see our API URL & Auth mode first, with a getting started section right below. On the left we will see a menu with Schema, Queries, Data Sources, and Settings links. We’ll discuss what these links in this menu do in some of the following steps.
Now that we have our API created, we need to do the following:
AppSync actually does alot of this for us as we will see in the following steps, the only thing we will need to do is create the schema.
In the left menu, click on Schema.
Now, clear out the commented out code and add the following schema and click Save:
type Recipe {
id: ID!
name: String!
ingredients: [String!]
instructions: [String!]
}
type Query {
fetchRecipe(id: ID!): Recipe
}
Now that the basic schema has been created, we need to a data source as well as add queries, mutations, and subscriptions. We also need resolvers to map the GraphQL request and response between the database and the schema.
AppSync can autogenerate all of this for us. To demonstrate this and create our resources, click on the Create Resources button to the top right of the schema.
Select Recipe as the type.
Now, we should see an area for us to decide the table name, and some additional code that will be added to our schema.
Go ahead and click Create.
After this we should see some new items added to our schema. Our schema should now look like this:
input CreateRecipeInput {
name: String!
ingredients: [String!]
instructions: [String]
}
input DeleteRecipeInput {
id: ID!
}
type Mutation {
createRecipe(input: CreateRecipeInput!): Recipe
updateRecipe(input: UpdateRecipeInput!): Recipe
deleteRecipe(input: DeleteRecipeInput!): Recipe
}
type Query {
fetchRecipe(id: ID!): Recipe
getRecipe(id: ID!): Recipe
listRecipes(first: Int, after: String): RecipeConnection
}
type Recipe {
id: ID!
name: String!
ingredients: [String!]
instructions: [String]
}
type RecipeConnection {
items: [Recipe]
nextToken: String
}
type Subscription {
onCreateRecipe(
id: ID,
name: String,
ingredients: [String!],
instructions: [String]
): Recipe
@aws_subscribe(mutations: ["createRecipe"])
onUpdateRecipe(
id: ID,
name: String,
ingredients: [String!],
instructions: [String]
): Recipe
@aws_subscribe(mutations: ["updateRecipe"])
onDeleteRecipe(
id: ID,
name: String,
ingredients: [String!],
instructions: [String]
): Recipe
@aws_subscribe(mutations: ["deleteRecipe"])
}
input UpdateRecipeInput {
id: ID!
name: String
ingredients: [String!]
instructions: [String]
}
Not only was the schema updated, but a table was created in DynamoDB and resolvers were created to map between the schema and the table. We’ll look at the resolvers in just a moment.
In the left menu, you should be able to now click on Data Sources and see the new table that was created. If you ever want to inspect this table, you can click on the table name under Resource and this will take you into the DynamoDB console to view the table.
Let’s test out this data by executing a mutation and then a query.
The mutation we will execute will be createRecipe
, and the query will be getRecipe
.
In the left menu, click on queries and create then execute the following mutation:
mutation createRecipe {
createRecipe(input: {
name: "Spicy Tuna Roll"
instructions: ["Chop tuna", "Make spicy sauce", "Mix spicy sauce with tuna" ]
ingredients: ["Tuna", "Mayonnaise", "Srirachi", "Soy sauce", "lime", "salt", "pepper"]
}) {
id
}
}
If the mutation was successful, you should see the returned id
on the right hand side.
Now, if you go back into your data sources, click on the table name, and then view the items tab, you should see the newly created. item.
Next, let’s perform a query on the data that is now in our table. We want to fetch all of the recipes from our API and view them in an array.
Below the mutation, add the following code:
query listRecipes {
listRecipes {
items {
id
name
instructions
ingredients
}
}
}
If you now click on the orange play button, you should see a dropdown of the two types of actions that can be performed.
In the dropdown, click on listRecipes to execute the query:
You should see the results of the query on the right hand side.
Now that we know that our schema and data source are working correctly, let’s take a look at the resolvers that were created when we created our data source.
Click back to our schema, and look to the right under Data Types.
If you look at these data types, you’ll see that we also have resolvers already created for us for all of our mutations and queries.
Scroll down below Mutation to createRecipe
and click on RecipeTable to the right under the Resolver field.
Here, we can see the resolver that is associated with the mutation.
Resolvers have three parts:
The request mapping template is where the GraphQL request is handled before being handed to the data source, in our case a DynamoDB table.
Mapping templates are written in a templating language called Apache Velocity Templating Language or VTL. AppSync uses VTL to translate GraphQL requests from clients into a request to your data source.
If you are familiar with other programming languages such as JavaScript, C, or Java, VTL should be fairly straightforward.
Let’s take a look at the request mapping template:
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id",
},
},
}
The fields are defined as follows:
If you’d like to learn more about how the mapping templates and VTL work, check out the documentation here. I’ve found that spending about an hour going through this documenatation got me to the point where I felt confident enough to start creating more complex custom queries and mutations.
You may have noticed that we are accessing a variable called $ctx
in the attributeValues
and the key
fields. $ctx
is short for $context
and either can be used interchangeably.
The $ctx
variable holds all of the contextual information for your resolver invocation. It has the following structure:
{
"arguments" : { ... },
"source" : { ... },
"result" : { ... },
"identity" : { ... }
}
Each $ctx
field is defined as follows:
To learn more about $context
, click here.
You may have also noticed the use of the $util
variable used in the mapping template.
The $util variable contains general utility methods that make it easier to work with data.
There are multiple $util
methods available, inculding a very handy function that allows us to generate ids on the fly: $util.autoId()
.
To learn more about $util
and see all of the available methods, click here.
Our API is complete and we can now begin interacting with it from our application!
The dependencies we will be using are Create React App to create our React app, glamor for styling, React Router, and uuid to create unique ids.
For our GraphQL client, we will be using a combination of React Apollo, AWS AppSync, AWS Appsync React, and graphql-tag.
To get started, let’s create a new React app and install our dependencies:
create-react-app recipes && cd recipes && npm i --save uuid react-router-dom glamor react-apollo aws-appsync aws-appsync-react graphql-tag
Next, we need to download the AppSync configuration file that we will be using to hook up our React application with the AppSync API.
We can do this by going into our AppSync dashboard and clicking on the the API name in the left menu, scrolling to the bottom, clicking on Web, and then clicking on Download below the Download the AWS AppSync config file:
Download and save this file as src/appsync.js
in your recipes application.
Next, let’s take a look at how we will structure our app.
We will need two routes:
Let’s go ahead and create the routes we will need. We’ll also create a Nav component that will serve as the static navigation component for navigating between these two routes:
touch src/Nav.js src/Recipes.js src/AddRecipe.js
We will also need queries, mutations, and subscriptions for this app. Let’s create a folder for each of these and a file to hold them:
mkdir src/mutations src/queries src/subscriptions
Next, let’s open index.js and update it with the following code:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from 'aws-appsync-react';
import { ApolloProvider } from 'react-apollo';
import appSyncConfig from './appsync';
// A
const client = new AWSAppSyncClient({
url: appSyncConfig.graphqlEndpoint,
region: appSyncConfig.region,
auth: {
type: appSyncConfig.authenticationType,
apiKey: appSyncConfig.apiKey,
}
});
// B
const WithProvider = () => (
<ApolloProvider client={client}>
<Rehydrated>
<App />
</Rehydrated>
</ApolloProvider>
);
ReactDOM.render(<WithProvider />, document.getElementById('root'));
The two main things that are happening are:
A. We are creating a new AppSync client and storing it in the client
variable, calling new AppSyncClient
and passing in the configuration we would like.
We are setting the auth type as API_KEY
, which comes from our appsync.js
configuration. This can also be set to Amazon Cognito user pools or Amazon Cognito Federated Identities. To learn more about how this configuration works, check out these docs.
B. Here we are using the ApolloProvider from react-apollo
to inject the client that we created into our application. By doing this, we now have access to our AppSync client anywhere in our entire application, or any component that is a child of the App component which is the entrypoint to our app.
Rehydrated will wait until the application cache has been read and is ready to use in the app before rendering the app. By default, this just shows some loading text, but this can be configured with our own UI like this if we would like:
const WithProvider = () => (
<ApolloProvider client={client}>
<Rehydrated
render={({ rehydrated }) => (
rehydrated ? <App /> : <strong>Your custom UI componen here...</strong>
)}
/>
</ApolloProvider>
);
Next, let’s create a basic router with two routes. Open App.js and update it to the following:
import React, { Component } from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Recipes from './Recipes'
import AddRecipe from './AddRecipe'
import Nav from './Nav'
class App extends Component {
render() {
return (
<div className="App">
<Router>
<div>
<Nav />
<Switch>
<Route exact path="/" component={Recipes} />
<Route path="/addrecipe" component={AddRecipe} />
</Switch>
</div>
</Router>
</div>
);
}
}
export default App;
As you can see above, we have two routes: /
that will render the Recipes
component, and /addrecipe
which will render the AddRecipe
component. We also have a Nav
component that’s displayed no matter which route we are currently viewing.
Nav is a pretty basic component that will only have a title and two links, along with some basic styling:
import React from 'react'
import { Link } from 'react-router-dom'
import { css } from 'glamor'
export default class Nav extends React.Component {
render() {
return (
<div {...css(styles.container)}>
<h1 {...css(styles.heading)}>Recipe App</h1>
<Link to='/' {...css(styles.link)}>Recipes</Link>
<Link to='/addrecipe' {...css(styles.link)}>Add Recipe</Link>
</div>
)
}
}
const styles = {
link: {
textDecoration: 'none',
marginLeft: 15,
color: 'white',
':hover': {
textDecoration: 'underline'
}
},
container: {
display: 'flex',
backgroundColor: '#00c334',
padding: '0px 30px',
alignItems: 'center'
},
heading: {
color: 'white',
paddingRight: 20
}
}
In the AddRecipe.js component we will be interacting with our AppSync client for the first time by performing a mutation to add data (a recipe) to our AppSync API, and a query to create an optimistic response.
Let’s go ahead and create the mutation & query we will be needing.
In src/mutations
, create a new file called CreateRecipe.js to hold our GraphQL mutation:
import gql from 'graphql-tag'
export default gql`
mutation createRecipe(
$name: String!,
$ingredients: [String!],
$instructions: [String!]
) {
createRecipe(input: {
name: $name, ingredients: $ingredients, instructions: $instructions,
}) {
id
name
instructions
ingredients
}
}
`
In src/queries
, create a new file called ListRecipes.js to hold our GraphQL query:
import gql from 'graphql-tag'
export default gql`
query listRecipes {
listRecipes {
items {
name
id
ingredients
instructions
}
}
}
`
Let’s take a look at how we will be wiring up the mutation for adding a new recipe.
The way we will be doing this is by wrapping the component that we would like the mutation to interact with, in our case the App component, with the graphql
higher order component from Apollo:
import { graphql } from 'react-apollo'
import CreateRecipe from './mutations/CreateRecipe'
import ListRecipes from './queries/ListRecipes'
class AddRecipe extends React.Component { /* class omitted for now */ }
export default graphql(CreateRecipe, {
props: props => ({
onAdd: recipe => props.mutate({
variables: recipe,
optimisticResponse: {
__typename: 'Mutation',
createRecipe: { ...recipe, __typename: 'Recipe' }
},
update: (proxy, { data: { createRecipe } }) => {
const data = proxy.readQuery({ query: ListRecipes });
data.listRecipes.items.push(createRecipe);
proxy.writeQuery({ query: ListRecipes, data });
}
})
})
})(AddRecipe)
The object returned from the props function will be received by the component as props. We declare an onAdd
function (received in the App component as this.props.onAdd
) that will pass in the object recipe
as an argument (recipe
will be the new recipe we would like to have created).
The optimisticResponse
and update
functions provide a way to update the local cache and our UI with the new data without having to wait for the data to get to the database and refresh our local data.
In the update function, we use the ListRecipes
query to read and write data to the cache.
In our actual App component, we will call onAdd
like this:
this.props.onAdd({
ingredients,
instructions,
name
})
Let’s put all of this together with form inputs and some state to create our AddRecipe component:
// src/AddRecipe.js
import React from 'react'
import { css } from 'glamor'
import { graphql } from 'react-apollo'
import uuidV4 from 'uuid/v4'
import CreateRecipe from './mutations/CreateRecipe'
import ListRecipes from './queries/ListRecipes'
class AddRecipe extends React.Component {
state = {
name: '',
ingredient: '',
ingredients: [],
instruction: '',
instructions: [],
}
onChange = (key, value) => {
this.setState({ [key]: value })
}
addInstruction = () => {
if (this.state.instruction === '') return
const instructions = this.state.instructions
instructions.push(this.state.instruction)
this.setState({
instructions,
instruction: ''
})
}
addIngredient = () => {
if (this.state.ingredient === '') return
const ingredients = this.state.ingredients
ingredients.push(this.state.ingredient)
this.setState({
ingredients,
ingredient: ''
})
}
addRecipe = () => {
const { name, ingredients, instructions } = this.state
this.props.onAdd({
ingredients,
instructions,
name
})
this.setState({
name: '',
ingredient: '',
ingredients: [],
instruction: '',
instructions: [],
})
}
render() {
return (
<div {...css(styles.container)}>
<h2>Create Recipe</h2>
<input
value={this.state.name}
onChange={evt => this.onChange('name', evt.target.value)}
placeholder='Recipe name'
{...css(styles.input)}
/>
<div>
<p>Recipe Ingredients:</p>
{
this.state.ingredients.map((ingredient, i) => <p key={i}>{ingredient}</p>)
}
</div>
<input
value={this.state.ingredient}
onChange={evt => this.onChange('ingredient', evt.target.value)}
placeholder='Ingredient'
{...css(styles.input)}
/>
<button onClick={this.addIngredient} {...css(styles.button)}>Add Ingredient</button>
<div>
<p>Recipe Instructions:</p>
{
this.state.instructions.map((instruction, i) => <p key={i}>{`${i + 1}. ${instruction}`}</p>)
}
</div>
<input
value={this.state.instruction}
onChange={evt => this.onChange('instruction', evt.target.value)}
placeholder='Instruction'
{...css(styles.input)}
/>
<button onClick={this.addInstruction} {...css(styles.button)}>Add Instruction</button>
<div {...css(styles.submitButton)} onClick={this.addRecipe}>
<p>Add Recipe</p>
</div>
</div>
)
}
}
export default graphql(CreateRecipe, {
props: props => ({
onAdd: recipe => props.mutate({
variables: recipe,
optimisticResponse: {
__typename: 'Mutation',
createRecipe: { ...recipe, __typename: 'Recipe' }
},
update: (proxy, { data: { createRecipe } }) => {
const data = proxy.readQuery({ query: ListRecipes });
data.listRecipes.items.push(createRecipe);
proxy.writeQuery({ query: ListRecipes, data });
}
})
})
})(AddRecipe)
const styles = {
button: {
border: 'none',
background: 'rgba(0, 0, 0, .1)',
width: 250,
height: 50,
cursor: 'pointer',
margin: '15px 0px'
},
container: {
display: 'flex',
flexDirection: 'column',
paddingLeft: 100,
paddingRight: 100,
textAlign: 'left'
},
input: {
outline: 'none',
border: 'none',
borderBottom: '2px solid #00dd3b',
height: '44px',
fontSize: '18px',
},
textarea: {
border: '1px solid #ddd',
outline: 'none',
fontSize: '18px'
},
submitButton: {
backgroundColor: '#00dd3b',
padding: '8px 30px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
opacity: .85,
cursor: 'pointer',
':hover': {
opacity: 1
}
}
}
The recipes component will be using the ListRecipes query to get the array of recipes from the database, and will also be using a subscription to listen for new data added to the API, subscribe to these changes, and update our local data when these changes happen.
The last file we need to create is the subscription to handle this functionality.
In src/subscriptions
, create a new file called NewRecipeSubscription.js:
import gql from 'graphql-tag'
export default gql`
subscription NewRecipeSub {
onCreateRecipe {
name
id
ingredients
instructions
}
}
`
Let’s take a look at how we will be wiring up the query & subscription to the component:
import { graphql } from 'react-apollo'
import ListRecipes from './queries/ListRecipes'
import NewRecipeSubscription from './subscriptions/NewRecipeSubscription'
class Recipes extends React.Component { /* class omitted for now */ }
export default graphql(ListRecipes, {
options: {
fetchPolicy: 'cache-and-network'
},
props: props => ({
recipes: props.data.listRecipes ? props.data.listRecipes.items : [],
subscribeToNewRecipes: params => {
props.data.subscribeToMore({
document: NewRecipeSubscription,
updateQuery: (prev, { subscriptionData: { data : { onCreateRecipe } } }) => {
return {
...prev,
listRecipes: {
__typename: 'RecipeConnection',
items: [onCreateRecipe, ...prev.listRecipes.items.filter(recipe => recipe.id !== onCreateRecipe.id)]
}
}
}
})
}
})
})(Recipes)
Like in the mutation example, the object returned from the props
function will be available in the component as this.props
. We declare two things:
The Recipes component will receive the array of recipes as props, map over them, and display them in the UI.
Let’s take a look at how all of this looks wired up together with some styling:
import React from 'react'
import { css } from 'glamor'
import { graphql } from 'react-apollo'
import ListRecipes from './queries/ListRecipes'
import NewRecipeSubscription from './subscriptions/NewRecipeSubscription'
class Recipes extends React.Component {
componentWillMount(){
this.props.subscribeToNewRecipes();
}
render() {
return (
<div {...css(styles.container)}>
<h1>Recipes</h1>
{
this.props.recipes.map((r, i) => (
<div {...css(styles.recipe)} key={i}>
<p {...css(styles.title)}>Recipe name: {r.name}</p>
<div>
<p {...css(styles.title)}>Ingredients</p>
{
r.ingredients.map((ingredient, i2) => (
<p key={i2} {...css(styles.subtitle)}>{ingredient}</p>
))
}
</div>
<div>
<p {...css(styles.title)}>Instructions</p>
{
r.instructions.map((instruction, i3) => (
<p key={i3} {...css(styles.subtitle)}>{i3 + 1}. {instruction}</p>
))
}
</div>
</div>
))
}
</div>
)
}
}
const styles = {
title: {
fontSize: 16
},
subtitle: {
fontSize: 14,
color: 'rgba(0, 0, 0, .5)'
},
recipe: {
boxShadow: '2px 2px 5px rgba(0, 0, 0, .2)',
marginBottom: 7,
padding: 14,
border: '1px solid #ededed'
},
container: {
display: 'flex',
flexDirection: 'column',
paddingLeft: 100,
paddingRight: 100,
textAlign: 'left'
}
}
export default graphql(ListRecipes, {
options: {
fetchPolicy: 'cache-and-network'
},
props: props => ({
recipes: props.data.listRecipes ? props.data.listRecipes.items : [],
subscribeToNewRecipes: params => {
props.data.subscribeToMore({
document: NewRecipeSubscription,
updateQuery: (prev, { subscriptionData: { data : { onCreateRecipe } } }) => {
return {
...prev,
listRecipes: {
__typename: 'RecipeConnection',
items: [onCreateRecipe, ...prev.listRecipes.items.filter(recipe => recipe.id !== onCreateRecipe.id)]
}
}
}
})
}
})
})(Recipes)
Now, start the app with npm start
and you should be able to create and view recipes.
That’s it! You have just created your first React + AppSync application.
AWS AppSync is extremely powerful, and in this tutorial we’ve just scratched the surface.
In addition to DynamoDB, AppSync also supports ElasticSearch & Lambda functions out of the box.
To continue learning and taking advantage of what it has to offer, I would focus on learning how the mapping templates work in depth.
Then, I would look at Authorization, possibly utilizing AWS Amplify or some other Authentication provider.
To view the final code for this app, click here
Hear me out - most JavaScript newsletters suck. That's why we made Bytes.
The goal was to create a JavaScript newsletter that was both insightful and entertaining. Over 80,000 subscribers later and well, reviews don't lie
I pinky promise you'll love it, but here's a recent issue so you can decide for yourself.
Delivered to over 80,000 developers every Monday