Praveen Jayarajan
9 min readOct 7, 2018

Serverless mobile with React Native and AWS Amplify

In this post I will go through the process of integrating an open-source React Native app with the Amazon Web Services (AWS) serverless backend infrastructure. I will be using the new AWS Amplify library developed by the AWS Mobile team.

React Native setup

brew install nodebrew install watchmannpm install -g react-native-cli

Make sure you have installed Xcode to get access to the iOS simulator + Android Studio +/- Genymotion for an Android simulator, or alternatively connect your hardware device. For more detailed platform specific instructions check out the link below:

https://facebook.github.io/react-native/docs/getting-started.html

Project setup

The app we are about to clone is a Bars App. It’s a simple app that finds your location and tells you what venues are nearby using the Google Places API. Users can view details about each venues price rating, opening hours, reviews, website, location, and phone number. Users can also get directions to the venue and add each venue to a favourites list. The video capture below taken from an iOS simulator will give you a clearer idea about what we will be re-creating:

https://youtu.be/4I9HTbN8E6o

To get started clone the repo:

git clone https://github.com/pjay79/BarsAppAmplify.git

Change to the project folder:

cd BarsAppAmplify

Add dependencies:

npm install or yarn

Project structure

Folder organisation is one of personal or team preference. My structure looks like this for this app:

folder structure
features

Amazon

To get started with Amazon first sign up to the AWS Free Tier:
https://aws.amazon.com/free/

AWS Amplify CLI setup

The new AWS Amplify CLI tool was introduced only around August 2018. It supersedes the AWS Mobile CLI.

npm install -g @aws-amplify/cli
amplify configure

The configure command will direct you to create a new IAM user via the AWS console, when prompted enter the accessKeyId and secretAccessKey, store these in a safe place, you can also assign this user an AWS Profile Name:

amplify configure
amplify init (in the project folder)
amplify init
amplify add auth
amplify add auth
amplify add api
amplify add api

The base schema.graphql file looks like this:

type Bar @model {
id: ID!
title: String!
content: String!
price: Int
rating: Float
}

This app will have a many-to-many connection between type Bar and type User. Currently AWS Amplify does not yet support many-to-many connections, hence the @connection directive which is used for specifying relationships between @model object types cannot be used. Update the schema.graphql file to look as follows.

type Bar @model {
id: ID!
createdAt: String
updatedAt: String
name: String!
phone: String
location: String
lat: String
lng: String
url: AWSURL
addedBy: ID!
users(first: Int, after: String): [Bar]
}

type BarMember @model {
id: ID!
createdAt: String
updatedAt: String
userId: ID!
barId: ID!
}

type User @model {
id: ID!
createdAt: String
updatedAt: String
username: String!
bars(first: Int, after: String): [Bar]
}

Important Step

amplify push

This command will update your cloud resources and add an aws-exports.js file to your project root directory. In your App.js file make sure this file is imported from the correct location.

Other directives

Note: AWS Amplify has has the following directives that can be used with AppSync:

@model: Used for storing types in Amazon DynamoDB.

@connection: Used to define different authorization strategies.

@auth: Used for specifying relationships between @model object types.

@searchable: Used for streaming the data of an @model object type to Amazon ElasticSearch Service.

AWS AppSync Schema

Go to the AWS Console and AWS AppSync under Services. Select the API that has been generated API for this app and go to the schema.

aws appsync schema

The schema that has been created needs some modification to allow for the many-to-many relationship between Bars and Users to work. Modify the schema as follows:

type Bar {
id: ID!
createdAt: String
updatedAt: String
name: String!
phone: String
location: String
lat: String
lng: String
url: AWSURL
website: AWSURL
addedBy: ID!
users(first: Int, after: String): BarUsersConnection
}

type BarMember {
id: ID
createdAt: String
updatedAt: String
userId: ID!
barId: ID!
}

type BarUsersConnection {
items: [User]
nextToken: String
}

input CreateBarInput {
id: ID!
name: String!
phone: String
location: String
lat: String
lng: String
url: AWSURL
website: AWSURL
addedBy: ID!
}

input CreateBarMemberInput {
userId: ID!
barId: ID!
}

input CreateUserInput {
id: ID!
username: String!
}

input DeleteBarInput {
id: ID
}

input DeleteBarMemberInput {
id: ID
}

input DeleteUserInput {
id: ID
}

type ModelBarConnection {
items: [Bar]
nextToken: String
}

input ModelBarFilterInput {
id: ModelIDFilterInput
createdAt: ModelStringFilterInput
name: ModelStringFilterInput
phone: ModelStringFilterInput
location: ModelStringFilterInput
lat: ModelStringFilterInput
lng: ModelStringFilterInput
url: ModelStringFilterInput
website: ModelStringFilterInput
addedBy: ModelIDFilterInput
and: [ModelBarFilterInput]
or: [ModelBarFilterInput]
not: ModelBarFilterInput
}

type ModelBarMemberConnection {
items: [BarMember]
nextToken: String
}

input ModelBarMemberFilterInput {
id: ModelIDFilterInput
createdAt: ModelStringFilterInput
userId: ModelIDFilterInput
barId: ModelIDFilterInput
and: [ModelBarMemberFilterInput]
or: [ModelBarMemberFilterInput]
not: ModelBarMemberFilterInput
}

input ModelBooleanFilterInput {
ne: Boolean
eq: Boolean
}

input ModelFloatFilterInput {
ne: Float
eq: Float
le: Float
lt: Float
ge: Float
gt: Float
contains: Float
notContains: Float
between: [Float]
}

input ModelIDFilterInput {
ne: ID
eq: ID
le: ID
lt: ID
ge: ID
gt: ID
contains: ID
notContains: ID
between: [ID]
beginsWith: ID
}

input ModelIntFilterInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
contains: Int
notContains: Int
between: [Int]
}

enum ModelSortDirection {
ASC
DESC
}

input ModelStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
}

type ModelUserConnection {
items: [User]
nextToken: String
}

input ModelUserFilterInput {
id: ModelIDFilterInput
createdAt: ModelStringFilterInput
username: ModelStringFilterInput
and: [ModelUserFilterInput]
or: [ModelUserFilterInput]
not: ModelUserFilterInput
}

type Mutation {
createBar(input: CreateBarInput!): Bar
updateBar(input: UpdateBarInput!): Bar
deleteBar(input: DeleteBarInput!): Bar
createBarMember(input: CreateBarMemberInput!): BarMember
updateBarMember(input: UpdateBarMemberInput!): BarMember
deleteBarMember(input: DeleteBarMemberInput!): BarMember
createUser(input: CreateUserInput!): User
updateUser(input: UpdateUserInput!): User
deleteUser(input: DeleteUserInput!): User
}

type Query {
getBar(id: ID!): Bar
listBars(filter: ModelBarFilterInput, limit: Int, nextToken: String): ModelBarConnection
getBarMember(userId: ID!, barId: ID!): BarMember
listBarMembers(filter: ModelBarMemberFilterInput, limit: Int, nextToken: String): ModelBarMemberConnection
getUser(id: ID!): User
listUsers(filter: ModelUserFilterInput, limit: Int, nextToken: String): ModelUserConnection
}

type Subscription {
onCreateBar: Bar
@aws_subscribe(mutations: ["createBar"])
onUpdateBar: Bar
@aws_subscribe(mutations: ["updateBar"])
onDeleteBar: Bar
@aws_subscribe(mutations: ["deleteBar"])
onCreateBarMember: BarMember
@aws_subscribe(mutations: ["createBarMember"])
onUpdateBarMember: BarMember
@aws_subscribe(mutations: ["updateBarMember"])
onDeleteBarMember: BarMember
@aws_subscribe(mutations: ["deleteBarMember"])
onCreateUser: User
@aws_subscribe(mutations: ["createUser"])
onUpdateUser: User
@aws_subscribe(mutations: ["updateUser"])
onDeleteUser: User
@aws_subscribe(mutations: ["deleteUser"])
}

input UpdateBarInput {
id: ID!
name: String
phone: String
location: String
lat: String
lng: String
url: AWSURL
website: AWSURL
}

input UpdateBarMemberInput {
id: ID!
userId: ID
barId: ID
}

input UpdateUserInput {
id: ID!
username: String
}

type User {
id: ID!
createdAt: String
updatedAt: String
username: String!
bars(first: Int, after: String): UserBarsConnection
}

type UserBarsConnection {
items: [Bar]
nextToken: String
}

AWS AppSync Resolvers

Again in the AppSync schema page there is a resolvers section on the right side. Go through an update the following resolvers:

Resolver for Bar.users: BarMemberTable

## Request{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "barId = :id",
"expressionValues" : {
":id" : {
"S" : "${ctx.source.id}"
}
}
},
"index": "barId-index",
"limit": $util.defaultIfNull(${ctx.args.first}, 20),
"nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.after, null))
}
## Response{
"items": $util.toJson($ctx.result.items),
"nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

Resolver for BarUsersConnection.items: UserTable

## Request
## Please remember to replace the hyphenated table name below with the one that was created for your app
#set($ids = [])
#foreach($user in ${ctx.source.items})
#set($map = {})
$util.qr($map.put("id", $util.dynamodb.toString($user.get("userId"))))
$util.qr($ids.add($map))
#end
{
"version" : "2018-05-29",
"operation" : "BatchGetItem",
"tables" : {
"User-rndmxxybyjfv5lvzou3767zbte": {
"keys": $util.toJson($ids),
"consistentRead": true
}
}
}
## Response
## Please remember to replace the hyphenated table name below with the one that was created for your app
#if( ! ${ctx.result.data} )
$util.toJson([])
#else
$util.toJson($ctx.result.data.User-uq7n63nywrc4tku2tzgx4mx75u)
#end

Resolver for User.bars: BarMemberTable

## Request{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "userId = :id",
"expressionValues" : {
":id" : {
"S" : "${ctx.source.id}"
}
}
},
"index": "userId-index",
"limit": $util.defaultIfNull(${ctx.args.first}, 20),
"nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.after, null))
}
## Response{
"items": $util.toJson($ctx.result.items),
"nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

Resolver for UserBarsConnection.items: BarTable

## Request
## Please remember to replace the hyphenated table name below with the one that was created for your app
#set($ids = [])
#foreach($bar in ${ctx.source.items})
#set($map = {})
$util.qr($map.put("id", $util.dynamodb.toString($bar.get("barId"))))
$util.qr($ids.add($map))
#end
{
"version" : "2018-05-29",
"operation" : "BatchGetItem",
"tables" : {
"Bar-rndmxxybyjfv5lvzou3767zbte": {
"keys": $util.toJson($ids),
"consistentRead": true
}
}
}
## Response
## Please remember to replace the hyphenated table name below with the one that was created for your app
#if( ! ${ctx.result.data} )
$util.toJson([])
#else
$util.toJson($ctx.result.data.Bar-uq7n63nywrc4tku2tzgx4mx75u)
#end

Resolver for Query.getBarMember: BarMember

## Request{
"version" : "2017-02-28",
"operation" : "Query",
"index" : "userId-index",
"query" : {
## Provide a query expression. **
"expression": "userId = :userId",
"expressionValues" : {
":userId" : $util.dynamodb.toDynamoDBJson($ctx.args.userId)
}
},
"filter" : {
"expression" : "barId = :barId",
"expressionValues" : {
":barId" : $util.dynamodb.toDynamoDBJson($ctx.args.barId)
}
},
}
## Response#if($ctx.result.items.size() > 0)
$util.toJson($ctx.result.items[0])
#else
null
#end

Resolver for Mutation.createBar: BarTable

Update the key only, leave the rest as it is.

"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},

Resolver for Mutation.createUser: UserTable

Update the key only, leave the rest as it is.

"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},

DynamoDB

From the AWS AppSync console select Data Sources and find the BarMember table. Create 2 indexes for this table, barId-index, and userId-index, with no sort keys and default settings. See example below:

dynamodb

Google Places API

Sign up to Google Places and get an API key.

google-places

Mapbox API

Sign up to Mapbox and get an API key.

mapbox

Add API keys

This project uses react-native-config to store API keys in an environment file. Creata a .env file in the project root directory, then add your Google Places API and Mapbox API keys here.

GOOGLE_PLACES_API_KEY=YOUR_KEY_GOES_HERE
MAPBOX_ACCESS_TOKEN=YOUR_KEY_GOES_HERE

Launch

Although this app will work on a simulator, connecting a hardware device for this will allow you to more easily access the geolocation, maps, directions, and features. There are some issues on android that have not been ironed out yet, so please refer to the Github repo.

Run on ios device

react-native run-ios — device "iPhone X"

Run on android device:

adb devices
react-native run-android — deviceId "myDeviceId"

Run on ios:

react-native run-ios

Run on android:

react-native run-android

If you are getting build errors try the following:

  • delete app from simulator or device and rebuild
  • erase all content and settings from simulator and rebuild
  • clean build folder in xcode and rebuild

Flow

I am in the process of migrating from PropTypes to using Flow. So you will see parts of the app using one or the other. To check for Flow errors:

yarn run flow start
yarn run flow status

Testing with Jest and Enzyme

There are some really basic snaphsot tests that have been created for some parts of the app using Jest and Enzyme. To check the current tests are working:

yarn run test

Additional information

React Apollo

In this app I have chosen to primarily use React Apollo’s graphql higher order component to connect queries, mutations, and subscriptions to the app. With React Apollo 2.1 you can use the new Query, Mutation, and Subscription components instead.

AWS Amplify API

In the Auth section of this app I have used AWS Amplify’s API and graphqlOperation helper. This API is effectively an alternative GraphQL client for working with queries, mutations, and subscriptions. It is great to use when you do not need offline support and the more advanced features of React Apollo.

AWS Appsync

With AWS AppSync you can combine React Apollo’s graphql higher order component with the graphqlMutation (offline support) and buildSubscription helpers. These take away some of the boilerplate code normally required to implement mutations and subscriptions. I have used the buildSubscription helper in this app.

Me:

I have been learning React Native for the last 12 months. Please check out my github for other open-source apps that I have been working on.

Bars App: https://github.com/pjay79/BarsAppAmplify

Github: https://www.github.com/pjay79

Twitter: @praveenj1979

Praveen Jayarajan

Software Developer. Javascript. React. React Native. Node.js. Docker. Cloud. 3x AWS Certified.