Building an iOS app with React Native and AWS Amplify

 
Source : GitHub

Source: GitHub

 

Words by David Johnson

The Challenge

Build an iOS application complete with authentication and multiple integrations.

The Timeline

Four two-week sprints.
Given the timeline, rolling our own backend with authentication and APIs was quickly dismissed as an option. Instead, we started to explore some out of the box solutions — but we did not have to look for long.

The Solution

AWS Amplify is a tool chain developed by Amazon that enables developers to rapidly provision serverless backends for web and mobile applications, and its feature set ticked all the boxes for us.

Out of the box it offers:

  • Analytics

  • Push Notifications

  • Machine Learning

  • VR

  • AR

  • Rest and GraphQL APIs

  • User management & Authentication

  • Hosting & Storage

  • Serverless Functions

  • Interactions/Chatbots


However, the best feature of Amplify in my opinion is the multi-environment setup. This enables developers to switch environment contexts as easily as they switch git branches.

This feature allows developers to modify/remove services in a sandboxed environment before pushing to production. It can also be run in “headless mode”, which enabled us to incorporate it into our CI/CD pipeline, meaning that environment changes can be deployed automatically once configuration changes have been made and committed.

While we didn’t use all the features of Amplify, let’s step through the features we did use, what’s great about them, and problems we faced along the way.

Cognito

Cognito is Amazon’s highly customisable user management and authentication system. Cognito supports OAuth 2.0, SAML 2.0, and OpenID connect. It also supports multi-factor authentication (MFA) and time-based one time passwords (TOTP), adding an extra layer of security.

Cognito user pools can also use federated identity providers such as Facebook, Google, Amazon, and SAML/OpenID.

AWS also provides a suite of React and React Native components, which allowed us to rapidly build out authentication flows for our application.

AppSync

AppSync is Amazon’s API and data service. It supports both GraphQL and REST endpoints, backed by NoSQL or Relational databases, or custom APIs. Cognito integration makes it very easy to lock down endpoints, even allowing granular user or role based field access.

We elected to use the GraphQL option, because at the time it offered more features out of the box, and seemed to be the more mature option. Creating an API with Amplify is as simple three step process.

1. Add the API

amplify add api

2. Create a simple GraphQL schema

type Event @model @auth(rules: [{allow: owner}]) { {

id: ID!
name: String
start: AWSTimestamp!
end: AWSTimestamp!
}

The @auth directive here tells AppSync that only the creator of a record can query or mutate it. Behind the scenes, AppSync adds an owner field to the model, which is the unique ID of the currently authenticated Cognito user. Perfect for our use case, as most of our endpoints should not be publicly accessible.

3. Push it and consume it

Simply run amplify push, and Amplify will create a DynamoDB database, create resolvers, and setup your endpoints. It will then also prompt you to generate all your GraphQL queries, mutations, and subscriptions, and generate an API file which you can include in your application.

import API, from '@aws-amplify/api'
import { getEvent, listEvents } from './graphql/queries'
import { createEvent, updateEvent } from './graphql/mutations'

class Events {
async listEvents() {
return await API.graphql(graphqlOperation(listEvents))
}

async getEvent(id: string) {
return await API.graphql(graphqlOperation(getEvent, {
id
}))
}

async createEvent(profile: CreateEventInput) {
return await API.graphql(graphqlOperation(createEvent, {
input: profile
}))
}

async updateEvent(profile: UpdateEventInput) {
return await API.graphql(graphqlOperation(updateEvent, {
input: profile
}))
}
}

Voila, we now have a data provider for our model which can be shared across our React Native and React web applications.

The Good

Simplicity and Scale

As you can tell from the above example, creating a secure API is incredibly easy. The same goes for everything else that comes with Amplify. This ease of use, combined with the React libraries allowed us to get to market quicker, and gave us a lot of confidence that our product would scale.

Powerful Technology

Recent additions to the Amplify toolchain such as Interactions (a wrapper for Lex, Amazon’s chatbot system), and Predictions (a wrapper for a number of Amazon’s artificial intelligence and machine learning products) give developers with limited experience the ability to build compelling experiences for their users.

Production Ready

Amplify doesn’t just have to be for prototyping/bootstrapping, AWS’ proven track record in delivering fast, scalable infrastructure means that you can just extend your prototype, rather than putting it in the bin and starting again.

Agile

Additionally, the Amplify development team are very active and responsive on Github, questions are answered, bugs are addressed quickly, and new versions are released almost bi-weekly.

The Not-So-Good

As the old saying goes: “If something seems too good to be true, it probably is.” For applications with limited complexity, and simple data structures, or for prototyping web or mobile apps, Amplify is perfect.

To be fair to Amazon, Amplify is a fairly new product, and was only about 12 months old when we started to use it - but there were a few areas where Amplify fell a little short.

Federated identities with React Native

I’ll preface this by saying that this is now fixed in the latest version of Amplify, however when attempting to build a login component that allowed customers to sign up or sign in with Facebook, we were unable to link the identity pool with our user pool.

This forced us to run with a traditional email/password authentication process. Luckily for us this provided an unexpected benefit, as we were able to do away with self-registration and launch to an invite-only audience — for whom we created Cognito accounts.

@connections can only be pushed one at a time

To create relationships between different GraphQL models (if you’re used to relational databases, think of them as primary/foreign keys), one can use the @connection decorator to enforce them. However, DynamoDB does not allow you to push more than one update to the GSI at once, which means that any attempt to push a schema with multiple @connections will fail.

As we had used Amplify in “headless” mode as part of our CI/CD pipeline, this meant that if we wanted to update our schema with multiple relationships, we had to push the changes to each environment manually, which almost defeated the purpose of integrating it into our CI/CD pipeline.

API list endpoints have a default record limit of 10

There was no mention of a default record limit in the documentation, so this caught us by surprise. We spent a few hours trying to get to the bottom of weird behaviour where datasets were coming back incomplete. No one could figure out why we could see them in the database. When we changed the records that were getting returned, we could see the updated versions, so it wasn’t a caching issue.

Eventually we discovered that inside each response mapping template, there is a directive to say that if a limit variable isn’t passed to the endpoint, then set a default of 10.

#set( $limit = $util.defaultIfNull($context.args.limit, 10) )
{
"version": "2017-02-28",
"operation": "Scan",
"filter": #if( $context.args.filter )
$util.transform.toDynamoDBFilterExpression($ctx.args.filter)
#else
null
#end,
"nextToken": #if( context.args.nextToken )
"$context.args.nextToken"
#else
null
#end,
}

Depending on your use case, there may be a number of ways around this. One is to simply pass a higher limit variable to the endpoints. However, there may be cases where you want everything returned regardless of how many records there may be (not something I’d recommend). Luckily Amplify allows you to override the default resolver request/response mapping templates - so you can remove that condition, and commit it to your codebase.

{
"version": "2017-02-28",
"operation": "Scan",
"filter": #if( $context.args.filter )
$util.transform.toDynamoDBFilterExpression($ctx.args.filter)
#else
null
#end,
"limit": #if($context.args.limit)
$context.args.limit
#else
null
#end,
"nextToken": #if( $context.args.nextToken )
"$context.args.nextToken"
#else
null
#end
}

Complex Logic

This isn’t so much a shortfall of Amplify, but of GraphQL itself. Complex relationships, aggregate queries, and subqueries aren’t really GraphQL’s jam - you can do it, but it has to be done at a resolver level.

Resolvers are what sit between your API endpoint and your database. They decide who can access this data, what data are they looking for, and how much data they are getting.

The problem with DynamoDB resolvers is that they are written in Velocity Template Language (VTL), which is a language that most developers are not familiar with — so we ran into a knowledge gap. Documentation is one way to address a knowledge gap, and Amazon provide some reasonably good documentation on resolvers. However, as a companion to documentation, most vendors generally supply example code.his is, by a wide margin, my preferred method of learning, and unfortunately for Amplify, it’s something that is sorely lacking.

Complex logic is probably the most important point to consider when evaluating whether Amplify is right for your product. I would recommend spiking out as much logic as you can up front. Build a POC with some custom resolvers, see if it works, but most importantly see how it feels. Is it a struggle? Are you hitting the limits of your talent or patience? It might be time to explore other options. Luckily the API wrapper also supports REST endpoints, so you can move all your complex logic to JavaScript and hook into DynamoDB (or another data source) directly.

The Verdict

Overall Amplify’s benefits make it a very attractive proposition for adding serverless backends to your applications. It’s quick, it’s cheap, it’s easy, and it’s backed by one of the biggest technology players in the world. 

Luckily for us, it’s not the only option. Google’s Firebase product provides similar functionality, at a similar price point. This competition hopefully equates to a healthy roadmap for both products for the foreseeable future, and a simpler, more productive life for developers, so we can focus on delivering our customers more value, quicker.

David Johnson is a Technical Lead at IE

Interested in IE’s Digital Capability? Let’s talk.