SST & AWS AppSync: GraphQL API Setup for React Native Made Simple

Posted in graphql, bff, aws-appsync, api on April 16, 2023 by Hemanta Sapkota ‐ 10 min read

SST & AWS AppSync: GraphQL API Setup for React Native Made Simple

Mobile app development relies heavily on APIs to provide data and functionality to the app. GraphQL APIs have gained popularity in recent years because they offer a more efficient way to fetch data and give frontend developers more control over what they receive. In this blog post, we'll be setting up an AppSync GraphQL API for a React Native app using SST on the backend and AWS Amplify on the frontend.

What is SST ?

SST is a serverless development framework that simplifies building and deploying serverless applications. It is a good choice for developers who want to quickly set up an API and don’t want to spend a lot of time configuring infrastructure. Compared to AWS CDK and the Serverless framework, SST has a smaller learning curve and allows developers to focus more on building their application rather than worrying about the underlying infrastructure. However, SST may not be as flexible or customizable as AWS CDK or the Serverless framework, which may be important for more complex applications. Ultimately, the choice between these frameworks will depend on the specific needs of the application and the preferences of the developer.

What is AWS Amplify ?

AWS Amplify is a development platform that makes it easy to add features like authentication, storage, and APIs to your app. With Amplify, you can quickly set up and manage backend services for your app, without having to worry about the underlying infrastructure. Amplify provides a wide range of tools and libraries for different app development frameworks, including React, React Native, Angular, and Vue. It also offers a variety of features, such as user authentication, push notifications, and analytics, which can help you build more powerful and engaging mobile apps.

Step 1: Setting up the Backend with SST

To get started, we need to install the SST CLI and create a new project.

# Install the SST CLI
npm install -g @serverless-stack/cli

# Create a new project with typescript
sst create-sst@latest my-app

# To create a new project with alternative langauge such as Golang
# npx create-sst@latest --template=other/go my-sst-app

Step 2: Install dependencies

Next, let’s install the dependencies for our newly created project.

# npm
npm install

# pnpm ( if you're using pnpm )
# pnpm install

# yarn ( if you're using yarn )
# yarn install

Step 3: Configure AppSync

The SST sample application comes with a preconfigured template that we can modify to fit our needs. To do this, just replace the contents of MyStack.ts with the following code snippet.

import { StackContext, AppSyncApi, Table } from "sst/constructs";

export function API({ stack }: StackContext) {
  const userProfileTable = new Table(stack, "UserProfileTable", {
    fields: {
      uid: "string",
    },
    primaryIndex: { partitionKey: "uid" },
  });
  const api = new AppSyncApi(stack, "AppSyncApi", {
    schema: "packages/functions/src/appsync/schema.graphql",
    defaults: {
      function: {
        bind: [userProfileTable],
      },
    },
    dataSources: {
      lambdaDataSource: "packages/functions/src/main.handler",
    },
    resolvers: {
      "Query    getUserProfile": "lambdaDataSource",
      "Mutation updateUserProfile":  "lambdaDataSource",
    },
  });
  api.attachPermissions(["dynamodb"])
  stack.addOutputs({
    ApiEndpoint: api.url,
  });
}

The code sets up an AppSync GraphQL API using SST on the backend. It defines a UserProfileTable using the Table construct and creates an AppSyncApi using the AppSyncApi construct with a schema property pointing to a GraphQL schema file. It also specifies a lambdaDataSource data source and sets up resolvers for the getUserProfile and updateUserProfile queries using the resolvers property. Finally, it attaches permissions for DynamoDB and adds an output for the API endpoint.

Step 3: GraphQL Schema Design

Create a new file in the path packages/functions/src/appsync/:

  1. If the appsync folder does not exist, create it.
  2. Inside the appsync folder, create a new file called schema.graphql.
type UserProfile {
    uid: String!
    email: String!
    name: String
}

input UserProfileInput {
    uid: String!
    email: String
    name: String
}

type Query {
    getUserProfile(input: UserProfileInput): UserProfile
}

type Mutation {
    updateUserProfile(input: UserProfileInput!): UserProfile
}

The code defines a GraphQL schema with a UserProfile type, which has uid, email, and name fields, and an UserProfileInput input type, which has uid, email, and name fields. It also sets up resolvers for the getUserProfile and updateUserProfile queries using the Query and Mutation types, respectively. The getUserProfile query takes an UserProfileInput input object and returns a UserProfile object, while the updateUserProfile mutation takes an UserProfileInput input object and returns a UserProfile object. The uid field in the UserProfile type and the UserProfileInput input type is marked with the ! symbol, indicating that it is required.

Step 4: Wire-up lambda resolvers

We have created two resolvers in our GraphQL schema - getUserProfileInput and updateUserProfileInput. It is important to note that we’ve opted to use a lambda resolver, which is a function that provides the data for our queries and mutations. In this section, we will write the code for these resolvers.

Step 4.1: getUserProfile

Create a new file called getUserProfile.ts and place it in packages/functions/src/appsync folder.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";
import UserProfile from "./UserProfile";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function getUserProfile(input: UserProfile): Promise<UserProfile | undefined> {
  const params = {
    Key: { uid: input.uid },
    TableName: Table.UserProfileTable.tableName,
  };

  const { Item } = await dynamoDb.get(params).promise();

  return Item as UserProfile;
}

This is a TypeScript function that retrieves a user profile from a DynamoDB table using AWS SDK. The function takes an input object of type UserProfile, which contains the uid (user ID) property. The function returns a promise that resolves to a UserProfile object or undefined if the user is not found.

Step 4.2: updateUserProfile

Create a new file called updateUserProfile.ts and place it in packages/functions/src/appsync folder.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";
import UserProfile from "./UserProfile";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function updateUserProfile(userProfile: UserProfile): Promise<UserProfile> {
  const params = {
    Key: { uid: userProfile.uid },
    ReturnValues: "UPDATED_NEW",
    UpdateExpression: "SET #name = :name, #email = :email",
    TableName: Table.UserProfileTable.tableName,
    ExpressionAttributeNames: { "#name": "name", "#email": "email" },
    ExpressionAttributeValues: { ":name": userProfile.name, ":email": userProfile.email },
  };

  await dynamoDb.update(params).promise();

  return userProfile as UserProfile;
}

This is a TypeScript function that updates a user profile in a DynamoDB table. It uses the AWS SDK to interact with the database, and takes a UserProfile object as input. The function constructs a parameter object for the update operation, which includes the table name, primary key value, and the new values for the name and email attributes. The function then executes the update operation and returns the updated user profile.

Step 5: Deploy

To deploy, run npm run dev . A successful output log should look like the following.

SST v2.3.6  ready!

➜  App:     my-sst-app
   Stage:   dev
   Console: https://console.sst.dev/my-sst-app/dev

|  API AppSyncApi/Api AWS::AppSync::GraphQLApi CREATE_COMPLETE
|  API AppSyncApi/Parameter_url AWS::SSM::Parameter CREATE_COMPLETE
|  API UserProfileTable/Table AWS::DynamoDB::Table CREATE_COMPLETE
|  API UserProfileTable/Parameter_tableName AWS::SSM::Parameter CREATE_COMPLETE
|  API AppSyncApi/Api/lambdaDataSource/ServiceRole AWS::IAM::Role CREATE_COMPLETE
|  API AppSyncApi/Lambda_lambdaDataSource/ServiceRole AWS::IAM::Role CREATE_COMPLETE
|  API CustomResourceHandler/ServiceRole AWS::IAM::Role CREATE_COMPLETE
|  API CustomResourceHandler AWS::Lambda::Function CREATE_COMPLETE
|  API AppSyncApi/Lambda_lambdaDataSource/ServiceRole/DefaultPolicy AWS::IAM::Policy CREATE_COMPLETE
|  API AppSyncApi/Lambda_lambdaDataSource AWS::Lambda::Function CREATE_COMPLETE
|  API AppSyncApi/Api AWS::AppSync::GraphQLSchema CREATE_COMPLETE
|  API AppSyncApi/Lambda_lambdaDataSource/EventInvokeConfig AWS::Lambda::EventInvokeConfig CREATE_COMPLETE
|  API AppSyncApi/Api/lambdaDataSource AWS::AppSync::DataSource CREATE_COMPLETE
|  API AppSyncApi/Api AWS::AppSync::ApiKey CREATE_COMPLETE
|  API AppSyncApi/Api/QuerygetUserProfileResolver AWS::AppSync::Resolver CREATE_COMPLETE
|  API AppSyncApi/Api/MutationupdateUserProfileResolver AWS::AppSync::Resolver CREATE_COMPLETE
|  API AppSyncApi/Api/lambdaDataSource/ServiceRole/DefaultPolicy AWS::IAM::Policy CREATE_COMPLETE
|  API AWS::CloudFormation::Stack CREATE_COMPLETE

✔  Deployed:
   API
   ApiEndpoint: https://n34ithnhbff35ijocgsqjqo6cu.appsync-api.ap-southeast-2.amazonaws.com/graphql

The logs show the output of deploying the backend with SST. The SST v2.3.6 ready! line indicates that the SST CLI is ready to be used. The subsequent lines show the progress of deploying the AppSync GraphQL API, including creating the GraphQL API, DynamoDB table, and resolvers, attaching permissions, and adding an output for the API endpoint. The Deployed: line indicates that the deployment was successful and shows the API endpoint URL.

Step 6: Test queries & mutations

To test queries and mutations, we need to create test data for the UserProfileTable in DynamoDB. We can do this using the AWS console or the AWS CLI. Here, we will use the SST console to create test data. Once we have the test data, we can use the GraphQL API endpoint URL from the SST deployment output and a GraphQL client like GraphQL Playground (which comes with SST Console) to test the queries and mutations.

Step 6.1: Create test data with SST Console

The SST framework includes a web console that allows for easy management of AWS resources. To create test data for DynamoDB, simply open the console by visiting https://console.sst.dev, navigate to the DynamoDB section on the left-hand side, and enter the following JSON test data:

{
  "email": "tim@example.com",
  "name": "Tim Example",
  "uid": "100002"
}

Step 6.2: Test query

Using the SST console, navigate to the GraphQL section on the left sidebar and input the following query in the GraphQL playground.

# A simple query to retrieve user profile
query MyQuery {
  getUserProfile(input: {uid: "100002"}) {
    uid
    name
  }
}

If it runs successfully, you’ll see the following output.

# Response
{
  "data": {
    "getUserProfile": {
      "uid": "100002",
      "name": "Tim Tam"
    }
  }
}

Step 6.3: Test mutation

Using the GraphQL playground once again, create a new query and input the following mutation.

mutation MyMutation {
  updateUserProfile(input: {
    uid: "100002",
    name: "Tim Example",
    email:"tim@google.com"}) {
    name
    email
  }
}

In GraphQL, a mutation is a type of operation that allows you to modify data on the server. Mutations are similar to queries, but they are used for creating, updating, or deleting data instead of just fetching it. Mutations are defined in the GraphQL schema and can be executed using a GraphQL client.

Upon successful execution, you should see the following output.

# Response
{
  "data": {
    "updateUserProfile": {
      "name": "Tim Example",
      "email": "tim@google.com"
    }
  }
}

Step 7: Setting up the Frontend with AWS Amplify

Step 7.1: Create a new react native project

To create a React Native project with Amplify, follow this guide - https://docs.amplify.aws/lib/project-setup/create-application/q/platform/react-native/.

Step 7.2: Install dependencies

yarn add aws-amplify amazon-cognito-identity-js @react-native-community/netinfo @react-native-async-storage/async-storage core-js
yarn add graphql-tag react-native-linear-gradient
yarn add aws-amplify aws-amplify-react-native

Step 7.2: App.tsx

import React from 'react';
import {StyleSheet, Text, View, ActivityIndicator} from 'react-native';
import Amplify from '@aws-amplify/core';
import gql from 'graphql-tag';
import {useEffect, useState} from 'react';
import {API, graphqlOperation} from 'aws-amplify';
import LinearGradient from 'react-native-linear-gradient';

Amplify.configure({
  aws_appsync_graphqlEndpoint: '<<insert_endpoint>>',
  aws_appsync_region: '<<insert_region>>',
  aws_appsync_authenticationType: 'API_KEY',
  aws_appsync_apiKey: '<<insert_api_key>>',
});

const GET_USER_PROFILE = gql`
  query MyQuery($input: UserProfileInput) {
    getUserProfile(input: $input) {
      uid
      name
      email
    }
  }
`;

function UserProfile() {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<
    | {
        uid: string;
        name?: string;
        email?: string;
      }
    | undefined
  >();

  useEffect(() => {
    setLoading(true);
    const fetchUserProfile = async () => {
      const result = await API.graphql(
        graphqlOperation(GET_USER_PROFILE, {
          input: {uid: '100002'},
        }),
      );
      setLoading(false);
      setData(result.data.getUserProfile);
    };
    fetchUserProfile();
  }, []);

  if (loading) {
    return <ActivityIndicator />;
  }

  return (
    <View style={styles.content}>
      <Text style={styles.text}>{data?.uid.trim()} |</Text>
      <Text style={styles.text}>{data?.name?.trim()} |</Text>
      <Text style={styles.text}>{data?.email?.trim()}</Text>
    </View>
  );
}

export default function App() {
  return (
    <View style={styles.container}>
      <LinearGradient
        colors={['#F44336', '#E91E63', '#FF4081']}
        style={styles.gradient}
      />
      <UserProfile />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  content: {
    flexDirection: 'row',
  },
  text: {
    fontSize: 18,
    paddingRight: 8,
    color: 'white',
    fontStyle: 'italic',
  },
  gradient: {
    position: 'absolute',
    width: '100%',
    height: '100%',
  },
});

This is a TypeScript code snippet that imports several libraries, including React, react-native, Amplify, graphql-tag, and Linear Gradient. It then sets up an Amplify configuration object and defines a GraphQL query using the gql function. The main functional component, UserProfile, makes a GraphQL API call to retrieve user data, and displays it as a set of Text components. Finally, the App component sets up a Linear Gradient before rendering the UserProfile component.

Step 7.3: Demo

Here’s a screenshot of the app we built in this walkthrough. The app fetches a record from our database using the getUserProfile query and displays the user ID, name, and email using React Native.

Conclusion

In this blog post, we set up an AppSync GraphQL API for a React Native app using SST on the backend and AWS Amplify on the frontend. We created a new AppSync API with a GraphQL schema, a DynamoDB table, and resolvers for the Query and Mutation types. We also configured Amplify with our AppSync API and added code to query and mutate data from the API in our React Native app. With these tools, we can efficiently build and deploy mobile apps that rely on powerful GraphQL APIs.

comments powered by Disqus