Build your own multi-user photo album app with React, GraphQL, and AWS Amplify — Part 1 of 3

Bootstrap our app, added authentication, and integrated a GraphQL API

Part 1 | Part 2 | Part 3
This is the first post in a three-part series that shows you how to build a scalable and highly available serverless web app on AWS that lets users upload photos to albums and share those albums privately with others.

An app like this requires a few moving parts:

  • Allowing user sign up and authentication, so we know who owns which photo albums, and so users can invite other users to their albums
  • Building an API server, so our front end has a way to load the appropriate albums and photos to show a given user
  • Storing data about albums, photos, and permissions of who can view what, so that our API has a fast and reliable place to query and save data to
  • Storing and serving photos, so we have a place to put all of the photos that users are uploading to albums
  • Automatically creating photo thumbnails, so we don’t need to deliver full-resolution photos when users browse a photo album’s list of photos

If we were to try and build scalable and highly-available systems to handle each of the above concerns on our own, we’d probably never get around to building our app! Fortunately, AWS provides services and tooling to handle a lot of the undifferentiated heavy lifting involved in building modern, robust applications. We’ll use a number of these services and tools in our solution, including:

If any or all of these services are new to you, don’t worry. These series will cover everything you need to know to get started using these services. And there’s no better way to learn than to build, so let’s get started!

What we’ll cover in this post

In this first post in the series, we’ll lay the groundwork for our application, and get ourselves to a place where we can create and view photo album records once a user is logged into the app. Here’s a list of the steps we’ll cover below:

  • Bootstrapping our web app with React
  • Adding user authentication
  • Deploying a GraphQL API to manage photo album data
  • Connecting our web app to the API, letting users create and list albums, with real-time updates

Pre-requisites

Before we begin, there are a few things you’ll want to make sure you have set up so you can follow along.

An AWS account - If you don’t have one yet, you’ll need an account with AWS. Creating an AWS account is free and gives you immediate access to the AWS Free Tier. For more information, or to sign up for an account, see https://aws.amazon.com/free/

Node.js installed - We’ll be using some JavaScript tools and packages which require Node.js (and NPM, which comes with Node.js) to be installed. You can download an installer or find instructions for installing Node.js at https://nodejs.org/en/download/

The AWS Amplify CLI installed and configured - The AWS Amplify CLI helps you quickly configure serverless backends and includes support for authentication, analytics, functions, REST/GraphQL APIs, and more. Follow the installation instructions at https://github.com/aws-amplify/amplify-cli. Windows users please note: The AWS Amplify CLI is currently only supported under the Windows Subsystem for Linux.

Bootstrapping our app

We’ll get things started by building a new React web app using the create-react-app CLI tool. This will give us a sample React app with a local auto-reloading web server and some helpful transpiling support for the browser like letting us use async/await keywords, arrow functions, and more. You can learn more about this tool at https://github.com/facebook/create-react-app. Running the tool uses a command installed alongside Node.js called npx which makes it easy to run a binary included inside an NPM package.

From your command line, navigate to a directory where you’d like to create a new directory for this project.

Run npx create-react-app photo-albums (you can use any name you want if you don’t like ‘photo-albums’, but I’ll assume you call it ‘photo-albums’ for these instructions).

That command will have created a new photo-albums directory with a bare bones React web app and a helpful script to run a local development web server. Before we start writing our UI, we’ll also include Semantic UI components for React to give us components that will help make our interface look a bit nicer.

Run npm install --save semantic-ui-react and integrate the default stylesheet by editing public/index.html and adding this stylesheet link to the <head> section: <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.3/semantic.min.css"></link>

Now let’s start our development server so we can make changes and see them refreshed live in the browser.

From within the photo-albums directory, run npm start. In a moment, you should see a browser window open to http://locahost:3000/ with a React logo and some other content.

Next, we’ll want to start with a clean slate, so let’s edit src/App.js and change it to display a simple ‘Hello World’ message. Replace the existing content of the file with the following:

// src/App.js
import React, { Component } from ‘react’;
import { Header } from 'semantic-ui-react';
class App extends Component { 
render() {
return (
<div>
<Header as='h1'>Hello World!</Header>
</div>
);
}
}
export default App;

At this point, the browser should automatically refresh and show a much simpler page, with just some text that says ‘Hello World’. It’s not much to look at yet, but it’s good to start with as little markup as possible so we’ll understand everything we add.

Adding user authentication with four lines of code

Now that we have a simple React app, let’s let users sign up and sign in to our app. They won’t be able to do anything yet, but it will be helpful to have this in place so that when we add in the ability to query our backend API, we’ll know which users are accessing our system.

The AWS Amplify CLI makes it easy for us to add cloud capabilities to our web and mobile apps, with SDKs available for React and React Native, iOS, and Android. To get started, we’ll create a new application and enable user authentication. We’ll then wire things up in our app using the open-source AWS Amplify JavaScript library, which the AWS Amplify CLI will take care of configuring for us; all we have to do is use it in our React app. AWS Amplify contains some nice abstractions for working with cloud services, and it has some helpful React components we’ll use in our app.

AWS Amplify’s default sign-in screen looks great

On your command line, make sure you’re inside the photo-albums directory and:

  1. Run amplify init
  2. Select your preferred editor
  3. Choose JavaScript and React when prompted
  4. Accept the default values when prompted for paths and build commands

This will create a new local configuration for us which we can use to set up an Amazon Cognito User Pool to act as the backend for letting users sign up and sign in (I’ll explain more about Amazon Cognito and what a User Pool is two paragraphs down). If you want to read more about this step, take a look at the ‘Installation and Configuration’ steps from the AWS Amplify Authentication guide.

When enabling user sign-in with the Amplify CLI, the default setup will require users to sign up with complex passwords and to confirm ownership of their email address before signing in. These are great settings for a production app and, while it’s completely possible to customize this configuration, let’s stick with these default settings.

Run amplify add auth to add authentication to the app. Select ‘Yes’ when asked if you’d like to use the default authentication and security configuration. Then, as the output text indicates, run amplify push. The Amplify CLI will take care of provisioning the appropriate cloud resources and it will update src/aws-exports.js with all of the configuration data we need to be able to use the cloud resources in our app.

Congratulations, you’ve just created a serverless backend for user registration and authorization capable of scaling to millions of users with Amazon Cognito. Amazon Cognito lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily. We just made a User Pool, which is a secure user directory that will let our users sign in with the username and password pair they create during registration. Amazon Cognito (and the Amplify CLI) also supports configuring sign-in with social identity providers, such as Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0. If you’d like to learn more, we have a lot more information on the Amazon Cognito Developer Resources page as well as the AWS Amplify Authentication documentation.

Now that we have our backend set up for managing registrations and sign-in, all we need to do is use the withAuthenticator higher-order react component from AWS Amplify to wrap our existing App component. This will take care of rendering a simple UI for letting users sign up, confirm their account, sign in, sign out, or reset their password.

We haven’t yet added the aws-amplify and aws-amplify-react modules to our app, so run npm install --save aws-amplify aws-amplify-react and then we can use them in our app. Let’s update src/App.js:

// src/App.js

// 1. NEW: Add some new imports
import Amplify from 'aws-amplify';
import aws_exports from './aws-exports';
import { withAuthenticator } from 'aws-amplify-react';
Amplify.configure(aws_exports);


// 2. EDIT: Wrap our exported App component with WithAuthenticator
export default withAuthenticator(App, {includeGreetings: true});

Looking back at our browser (after re-starting our server), we’ve now got a simple sign-up / sign-in form. Try signing up in the app by providing a username, password, and an email address you can use if you need to reset your password. You’ll be taken to a screen asking you to confirm a code. This is because Amazon Cognito wants to verify a user’s email address before it lets them sign in. Go check your email and you should see a confirmation code message. Copy and paste that code into your app and you should then be able to log in with the username and password you entered during sign up. Once you sign in, the form disappears and you can see our App component rendered below a header bar that contains your username and a ‘Sign Out’ button.

This is a pretty simple authentication UI, but there’s a lot you can do to customize it, including replacing parts with your own React components or using a completely hosted UI that can redirect back to your app. See the Customization section of the AWS Amplify Authentication Guide for more information.

Creating a serverless GraphQL API backend

Now that we have authenticated users, let’s make an API for creating albums. These albums won’t have any photos in them just yet, just a name and an association with the username that created them, but it’s another clear step toward putting our app together.

To build our API we’ll use AWS AppSync, a managed GraphQL service for building data-driven apps. If you’re not yet familiar with the basics of GraphQL, I suggest you take a few minutes and check out https://graphql.github.io/learn/ before continuing, or use the site to refer back to when you have questions as you read along.

AWS AppSync: Rapid prototyping and development with GraphQL

AWS AppSync also makes adding real-time and offline capabilities to your apps pretty easy, and we’ll add a real-time subscription at the end of this post, but let’s start with the basics: creating an AWS AppSync API, adding a simple GraphQL schema, and connecting it with DynamoDB for storing our data in a NoSQL database. Again, the Amplify CLI streamlines this process quite a bit.

From your terminal, run amplify add api and respond to the prompts like this:

$ amplify add api 
? Please select from one of the below mentioned services: GraphQL
? Provide API name: photoalbums
? Choose an authorization type for the API: Amazon Cognito User Pool
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: One-to-many relationship
? Do you want to edit the schema now? Yes
Please edit the file in your editor: photo-albums/amplify/backend/api/photo-albums/schema.graphql

At this point, your code editor should show you a sample one-to-many GraphQL schema, annotated with some @model and @connection bits. The Amplify CLI will transform this schema into a set of data sources and resolvers to run on AWS AppSync. You can learn more about Amplify’s GraphQL transforms here.

Below is a schema that will suit our needs for storing and querying Albums and Photos. Copy and paste this into your editor, replacing the example schema content, and remembering to save the file:

# amplify/backend/api/photo-albums/schema.graphql

type Album @model {
id: ID!
name: String!
owner: String!
photos: [Photo] @connection(name: "AlbumPhotos")
}

type Photo @model {
id: ID!
album: Album @connection(name: "AlbumPhotos")
bucket: String!
fullsize: PhotoS3Info!
thumbnail: PhotoS3Info!
}

type PhotoS3Info {
key: String!
width: Int!
height: Int!
}

Now return to your command line and press Enter once to continue.

Finally, run amplify push and confirm you’d like to continue with the updates. Then wait a few moments while the new resources are provisioned in the cloud.

Open the AWS AppSync web console, find the API matching the name you entered above, and click it. Now we can start poking around with the API.

The really cool thing is, at this point, without having to write any code, we now have a GraphQL API that will let us perform CRUDL operations on our Album and Photo data types! But don’t worry, the way AWS AppSync is resolving fields into data isn’t hidden from us. Each resolver that was automatically generated is available for us to edit as we see fit, and we’ll get to that later. For now, let’s try out adding some albums and then retrieving them all as a list.

Click over to the ‘Queries’ section in the sidebar. This area is AWS AppSync’s interactive query explorer. We can write queries and mutations here, execute them, and see the results. It’s a great way to test things out to make sure our resolvers are working the way we expect.

Before we can issue queries, we’ll need to authenticate (because our AppSync API is configured to authenticate users via the Amazon Cognito User Pool we set up when we configured the authentication for our app.

The AWS AppSync web console allows you to authenticate with your API before issuing queries
  1. In the Queries section of the AppSync web console, notice there is a ‘Login with User Pools’ button at the top of the query editor. Click the button.
  2. In the ‘ClientId’ field, we’ll need to paste in a value from our Cognito User Pool configuration. One place to find the right value is to look in src/aws-exports.js in our React web app. This file was generated by the Amplify CLI and contains lots of configuration values for our app. The one we want to copy and use for this field is aws_user_pools_web_client_id.
  3. In the ‘Username’ and ‘Password’ fields, use the values you specified when you created a username in our React app above.
  4. Click ‘Login’. You should now be able to try out the following mutations and queries.

Add a new album by copy/pasting the following and running the query:

mutation {
createAlbum(input:{name:"First Album", owner:"Someone"}) {
id
name
}
}

Add another album by editing and re-running your createAlbum mutation with another album name:

mutation {
createAlbum(input:{name:"Second Album", owner:"Someone"}) {
id
name
}
}

List our albums by running this query:

query {
listAlbums {
items {
id
name
owner
}
}
}

As you can see, we’re able to read and write data through GraphQL queries and mutations. But, things aren’t exactly as we’d like. In the code that was generated, we’re required to specify an owner string for each Album we create. Instead, it would be great if the system would automatically set the owner field to the username of whichever user sent the createAlbum mutation. It’s not hard to make this change, so let’s get it out of the way now before we connect our web app to our AWS AppSync API endpoint.

Customizing our first GraphQL resolver in AWS AppSync

AWS AppSync supports multiple ways of resolving data for our fields. For fetching data from DynamoDB and Amazon ElasticSearch, we can resolve our requests and responses using the Apache Velocity Template Language, or VTL. We can also resolve fields using an AWS Lambda function, but since our current needs are so simple, we’ll stick to resolving from DynamoDB using VTL for now.

The resolvers that AWS AppSync generated for us are just a way to translate our GraphQL queries and mutations into DynamoDB operations.

At first glance, interacting with DynamoDB via these templates can seem a bit weird, but there’s only a few concepts you need to get in order to work with them effectively. AWS AppSync resolver mapping templates are evaluated by a VTL engine and end up as a JSON string that’s used to make API calls to DynamoDB. Each DynamoDB operation has a corresponding set of values it expects in the JSON, based on whatever that API operation is about. See the Resolver Mapping Template Reference for DynamoDB for more information. VTL templates can use variables and logic to control the output that gets rendered. There are lots of examples in the Resolver Mapping Template Programming Guide. Any arguments passed in from our GraphQL queries or mutations are available in the $ctx.args variable, and user identity information is available in the $ctx.identity variable. You can read more about these, and other variables, in the Resolver Mapping Template Context Reference.

Since we want to change how our createAlbum mutation interfaces with DynamoDB, let’s open that up resolver make make an edit. It would be great if we could edit a resolver file locally on our development machine, then do another push with the Amplify CLI, but at this time, we can’t do this via the CLI yet, so we’ll have to make the change manually in the AWS AppSync web console, and take care not to run any further API pushes from the Amplify CLI, which would overwrite the changes we want to make. Here’s how to make the edit we want:

  1. Click ‘Schema’ in the AWS AppSync sidebar
  2. On the right side of the screen in the Resolvers column, scroll down to the Mutation section (or use the search box, typing ‘mutation’), find the createAlbum(…):Album listing and click the AlbumTable link to view and edit this resolver
  3. Near the lines at the top that look like $util.qr(…) add another similar line to inject the current user’s username in as another argument to the mutation’s input: $util.qr($context.args.input.put("owner", $context.identity.username))
  4. Click ‘Save Resolver’

Also, since we’re no longer passing a value for owner when we create an Album, we’ll update the Schema to only require a ‘name’ for albums.

Edit the Schema, replacing the CreateAlbumInput type with the following definition, and click ‘Save Schema’

input CreateAlbumInput {
name: String!
}

Finally, we can test out creating another Album in the Queries tool to see if our modified resolver works correctly:

mutation {
createAlbum(input: {name:"Album with username from context"}) {
id
name
owner
}
}

That mutation should have returned successfully. Notice the album’s owner field contains the username we logged in with.

If you’re curious to see more examples of how AWS AppSync translates GraphQL queries and mutations into commands for DynamoDB, take a few minutes to examine the other auto-generated resolvers as well. Just click on any of the Resolver links you see in the Data Types sidebar of the AWS AppSync Schema view.

Connecting our app to the GraphQL API

At this point, we have a web app that authenticates users and a secure GraphQL API endpoint that lets us create and read Album data. It’s time to connect the two together! The Apollo client is a popular choice for working with GraphQL endpoints, but there’s a bit of boilerplate code involved when using Apollo, so let’s start with something simpler.

As we saw above, AWS Amplify is an open source JavaScript library that makes it very easy to integrate a number of cloud services into your web or React Native apps. We’ll start by using its Connect React component to take care of automatically querying our GraphQL API and providing data for our React components to use when rendering.

The Amplify CLI has already taken care of making sure that our src/aws-exports.js file contains all of the configuration we’ll need to pass to the Amplify JS library in order to talk to the AppSync API. All we’ll need to do is add some new code to interact with the API.

We’ll create some new components to handle data fetching and rendering of our Albums. In a real app, we’d create separate files for each of our components, but here we’ll just keep everything together so we can see all of the code in one place.

Rendering a list of albums in our app

Make the following changes to src/App.js:

// src/App.js
// 1. NEW: Add some new imports
import { Connect } from 'aws-amplify-react';
import { graphqlOperation } from 'aws-amplify';
import { Grid, Header, Input, List, Segment } from 'semantic-ui-react';
// 2. NEW: Create a function we can use to 
// sort an array of objects by a common property
function makeComparator(key, order='asc') {
return (a, b) => {
if(!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) return 0;

const aVal = (typeof a[key] === 'string') ? a[key].toUpperCase() : a[key];
const bVal = (typeof b[key] === 'string') ? b[key].toUpperCase() : b[key];

let comparison = 0;
if (aVal > bVal) comparison = 1;
if (aVal < bVal) comparison = -1;

return order === 'desc' ? (comparison * -1) : comparison
};
}


// 3. NEW: Add an AlbumsList component for rendering
// a sorted list of album names
class AlbumsList extends React.Component {
albumItems() {
return this.props.albums.sort(makeComparator('name')).map(album =>
<li key={album.id}>
{album.name}
</li>);
}

render() {
return (
<Segment>
<Header as='h3'>My Albums</Header>
<List divided relaxed>
{this.albumItems()}
</List>
</Segment>
);
}
}


// 4. NEW: Add a new string to query all albums
const ListAlbums = `query ListAlbums {
listAlbums(limit: 9999) {
items {
id
name
}
}
}`;


// 5. NEW: Add an AlbumsListLoader component that will use the
// Connect component from Amplify to provide data to AlbumsList
class AlbumsListLoader extends React.Component {
render() {
return (
<Connect query={graphqlOperation(ListAlbums)}>
{({ data, loading, errors }) => {
if (loading) { return <div>Loading...</div>; }
if (!data.listAlbums) return;

return <AlbumsList albums={data.listAlbums.items} />;
}}
</Connect>
);
}
}


// 6. EDIT: Change the App component to look nicer and
// use the AlbumsListLoader component
class App extends Component {
render() {
return (
<Grid padded>
<Grid.Column>
<AlbumsListLoader />
</Grid.Column>
</Grid>
);
}
}

If you check back in the browser after making these changes, you should see a list of the albums you’ve created so far.

The real magic here comes from AWS Amplify’s Connect component (which we imported from the aws-amplify-react package). All we need to do is pass this component a GraphQL query operation in its query prop. It takes care of running that query when the component mounts, and it passes information down to a child function via the data, loading, and errors arguments. We use those values to render appropriately, either showing some loading text, dumping an error message to help us debug, or passing the successfully fetched data to our AlbumsList component.

The listAlbums query we’re above using passes in a very high limit argument. This is because I’ve decided to load all of the albums in one request and sort the albums alphabetically on the client-side (instead of dealing with paginated DynamoDB responses). This keeps the AlbumsList code pretty simple, so I think it’s worth the trade off in terms of performance or network cost.

That takes care of fetching and rendering our list of Albums. Now let’s make a component to add new Albums. Make the following changes to src/App.js:

// src/App.js

// 1. EDIT: Update our import for aws-amplify to
// include API and graphqlOperation
// and remove the other line importing graphqlOperation
import Amplify, { API, graphqlOperation } from 'aws-amplify';

// 2. NEW: Create a NewAlbum component to give us
// a way of saving new albums
class NewAlbum extends Component {
constructor(props) {
super(props);
this.state = {
albumName: ''
};
}

handleChange = (event) => {
let change = {};
change[event.target.name] = event.target.value;
this.setState(change);
}

handleSubmit = async (event) => {
event.preventDefault();
const NewAlbum = `mutation NewAlbum($name: String!) {
createAlbum(input: {name: $name}) {
id
name
}
}`;

const result = await API.graphql(graphqlOperation(NewAlbum, { name: this.state.albumName }));
console.info(`Created album with id ${result.data.createAlbum.id}`);
}

render() {
return (
<Segment>
<Header as='h3'>Add a new album</Header>
<Input
type='text'
placeholder='New Album Name'
icon='plus'
iconPosition='left'
action={{ content: 'Create', onClick: this.handleSubmit }}
name='albumName'
value={this.state.albumName}
onChange={this.handleChange}
/>
</Segment>
)
}
}


// 3. EDIT: Add NewAlbum to our App component's render
class App extends Component {
render() {
return (
<Grid padded>
<Grid.Column>
<NewAlbum />
<AlbumsListLoader />
</Grid.Column>
</Grid>
);
}
}

Check the browser again and you should be able to create new albums with a form.

Finally, we can have our list of albums automatically refresh by taking advantage of AppSync’s real-time data subscriptions which trigger when mutations are made (by anyone). The easiest way to do this is to define a subscription in our schema so AWS AppSync will let us subscribe to createAlbum mutations, and then add the subscription to our Connect component that provides data for the AlbumList.

Our GraphQL schema already contains a Subscription type with a bunch of subscriptions that were auto-generated back when we had AWS AppSync create the resources (DynamoDB table and AWS AppSync resolvers) for our Album type. One of these is already perfect for our needs: the onCreateAlbum subscription.

Add subscription and onSubscriptionMsg properties to our Connect component to subscribe to the onCreateAlbum event data and update the data for AlbumsList accordingly. The content for the subscription property looks very similar to what we provided for the query property previously; it just contains a query specifying the subscription we want to listen to and what fields we’d like back when new data arrives. The only slightly tricky bit is that we also need to define a handler function to react to new data from the subscription, and that function needs to return a new set of data that the Connect component will use to refresh our ListAlbums component.

Here’s an updated version of our AlbumsListLoader component (and a new string containing our subscription) with these changes incorporated. Make the following changes to src/App.js:

// src/App.js
// 1. NEW: Add a string to store a subcription query for new albums
const SubscribeToNewAlbums = `
subscription OnCreateAlbum {
onCreateAlbum {
id
name
}
}
`;


// 2. EDIT: Update AlbumsListLoader to work with subscriptions
class AlbumsListLoader extends React.Component {

// 2a. NEW: add a onNewAlbum() function
// for handling subscription events
    onNewAlbum = (prevQuery, newData) => {
// When we get data about a new album,
// we need to put in into an object
// with the same shape as the original query results,
// but with the new data added as well
let updatedQuery = Object.assign({}, prevQuery);
updatedQuery.listAlbums.items = prevQuery.listAlbums.items.concat([newData.onCreateAlbum]);
return updatedQuery;
}

render() {
return (
<Connect
query={graphqlOperation(ListAlbums)}

// 2b. NEW: Listen to our
// SubscribeToNewAlbums subscription
                subscription={graphqlOperation(SubscribeToNewAlbums)} 
                // 2c. NEW: Handle new subscription messages
                onSubscriptionMsg={this.onNewAlbum}
>

{({ data, loading, errors }) => {
if (loading) { return <div>Loading...</div>; }
if (errors.length > 0) { return <div>{JSON.stringify(errors)}</div>; }
if (!data.listAlbums) return;

return <AlbumsList albums={data.listAlbums.items} />;
}}
</Connect>
);
}
}

With these changes in place, if you try creating a new album, you should see it appear at the bottom of our list of album names. What’s even cooler, is that if you add a new album in another browser window, or in the AWS AppSync Query console, you’ll see it appear automatically in all of your open browser windows. That’s the real-time power of subscriptions at work!

There’s more to come

Whew! We’ve come a long way since the beginning of this post. We created a new React app with user sign up and authentication, created a GraphQL API to handle persisting and querying data, and we wired these two parts together to create a real-time data-driven UI. Give yourself a pat on the back!

In future posts, we’ll continue on and add photo album uploads, thumbnail generation, querying for all the photos in an album, paginating photo results, fine-grained album security settings, and deploying our app to a CDN.

If you’d like to be notified when new posts come out, please follow me on Twitter: Gabe Hollombe. That’s also the best way to reach me if you have any questions or feedback about this post.

Part 1 | Part 2 | Part 3
This is the first post in a three-part series that shows you how to build a scalable and highly available serverless web app on AWS that lets users upload photos to albums and share those albums privately with others.

Bootstrapping what we’ve built so far

If you’d like to just check out a repo and launch the app we’ve built so far, check out this repo on GitHub and use the blog-post-part-one tag, linked here: https://github.com/gabehollombe-aws/react-graphql-amplify-blog-post/tree/blog-post-part-one. Follow the steps in the README to configure and launch the app.