DEV Community 👩‍💻👨‍💻

Roman Mühlfeldner
Roman Mühlfeldner

Posted on

How to use an AWS Amplify GraphQL API with a React TypeScript Frontend

Introduction

AWS Amplify is a development platform for mobile and web applications. It is built-in Amazon Web Services (AWS) and scaffolds different AWS Services, like e.g Lambda functions, Cognito User Pools and an AppSync GraphQL API. This takes out the pain of manually setting up an AWS Infrastructure for a mobile an web applications, resulting in faster development speed. Amplify even has an own documentation site and is open source

This post will show you how to setup a GraphQL API with TypeScript code generation and how to use it in a React frontend application.

AWS Account

Since Amplify is an AWS service it is required to sign in to the AWS Console. If you don't have an account, create one. Note: A credit card is required. But due to the pandemic, AWS Educate was introduced so you may be able to signup for an account without a credit card required. However, this tutorial will not cost anything when published to the cloud.

Setup React project

For the React frontend we will use a simple Create React App (CRA):
Run these CLI commands to create it and add the Amplify library

npx create-react-app amplify-typescript-demo --template typescript
cd amplify-typescript-demo
npm install --save aws-amplify

Setup Amplify

Make sure, the Amplify CLI is globally installed and configured.
The official documentation describes it very well and even has a video: Install and configure Amplify CLI

After the CLI is configured properly, we can initialize Amplify in our project:

amplify init

This command wil initialize Amplify inside our project and it needs some information. Since we have a basic CRA App, we can simply just press enter and continue with the default options:

 Enter a name for the project `amplifytypescriptdem`
 Enter a name for the environment `dev`
 Choose your default editor: `Visual Studio Code`
 Choose the type of app that you\'re building `javascript`
 What javascript framework are you using `react`
 Source Directory Path: `src`
 Distribution Directory Path: `build`
 Build Command: `npm run-script build`
 Start Command: `npm run-script start`
 Do you want to use an AWS profile? `Yes`
 Please choose the profile you want to use `amplify-workshop-use`

Add a GraphQL API

Now the GraphQL API can be added by running:

amplify add api

This will starty by asking some questions:

 Please select from one of the below mentioned services: `GraphQL`
 Provide API name: `DemoAPI`
 Choose the default authorization type for the API: `API key`
 Enter a description for the API key: My Demo API
 After how many days from now the API key should expire (1-365): `7`
 Do you want to configure advanced settings for the GraphQL API: `No, I am done.`
 Do you have an annotated GraphQL schema? `No`
 Do you want a guided schema creation? `Yes`
 What best describes your project: `Single object with fields (e.g., “Todo” with ID, name, description)`
 Do you want to edit the schema now? `No`

This will generate a GraphQL API. Open amplify/backend/api/DemoAPI/schema.graphql to view the model.
This should contain a basic ToDo model:

type Todo @model {
  id: ID!
  name: String!
  description: String
}

Mock and test the API

The API is ready to be tested! We don't have to configure any Lambda functions or AppSync manually. Everything's managed by Amplify.
To test the API we don't even have to deploy it in the cloud. Amplify has the ability to mock the whole API locally:

amplify mock api

Again, this will also ask some questions. And here comes the TypeScript part. This call will auto-generate TypeScript models for our React app. Simply choose typescript and go ahead with the default options:

 Choose the code generation language target `typescript`
 Enter the file name pattern of graphql queries, mutations and subscriptions `src/graphql/**/*.ts`
 Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions `Yes`
 Enter maximum statement depth [increase from default if your schema is deeply nested] `2`
 Enter the file name for the generated code `src/API.ts`
 Do you want to generate code for your newly created GraphQL API `Yes`

Finally, you should get a message with the local address on which the API is running:

AppSync Mock endpoint is running at http://192.168.0.143:20002

Open that address in the browser and you should see GraphiQL.

Create and list ToDos

Here are some Mutations and Queries to create and test demo data:

mutation CreateTodo {
  createTodo(
    input: { name: "Blog Post", description: "Write a Blog Post about Amplify" }
  ) {
    description
    name
  }
}

mutation CreateTodo2 {
  createTodo(
    input: { name: "Dinner", description: "Buy groceries and cook dinner" }
  ) {
    description
    name
  }
}

query ListTodos {
  listTodos {
    items {
      name
      description
    }
  }
}

Use the API in the React app

First step is to import Amplify and configure it. The config object is imported from ./aws-exports. This file is generated by Amplify and should not be edited manually or pushed to e.g. GitHub!

import Amplify from 'aws-amplify';
import config from './aws-exports';
Amplify.configure(config);

Wrap Amplify API.graphql

Amplify provides a functionality to consume the GraphQL API, so you don't have to use another GraphQL Client like Apollo-Client.
Just create a small generic wrapper for it to be a little bit more type safe:

import { API, graphqlOperation } from "aws-amplify";
import { GraphQLResult, GRAPHQL_AUTH_MODE } from "@aws-amplify/api";

export interface GraphQLOptions {
  input?: object;
  variables?: object;
  authMode?: GRAPHQL_AUTH_MODE;
}

async function callGraphQL<T>(query: any, options?: GraphQLOptions): Promise<GraphQLResult<T>> {
  return (await API.graphql(graphqlOperation(query, options))) as GraphQLResult<T>
}

export default callGraphQL;

The function callGraphQL<T> is generic and just returns the result of API.graphql(...). The Result is from the type GraphQLResult<T>. Without this small wrapper we would always have to cast it to GraphQLResult<T>.

Query List ToDos

Create a new folder src/models and inside a file todo.ts. This is the file which contains the frontend model for our ToDo and a function to map the objects:

import { ListTodosQuery } from "../API";
import { GraphQLResult } from "@aws-amplify/api";

interface Todo {
  id?: string;
  name?: string;
  description?: string;
}

function mapListTodosQuery(listTodosQuery: GraphQLResult<ListTodosQuery>): Todo[] {
  return listTodosQuery.data?.listTodos?.items?.map(todo => ({
    id: todo?.id,
    name: todo?.name,
    description: todo?.description
  } as Todo)) || []
}

export default Todo;
export { mapListTodosQuery as mapListTodos }

What is happening here? First, we import ListTodosQuery from '../API' and GraphQLResult. API.ts is generated by the Amplify CLI and contains the GraphQL API types. GraphQLResult is the generic interface which the GraphQL API returns.
Next, we have a simple Todo interface and a function mapListTodosQuery. This maps an object from type GraphQLResult<ListTodosQuery> to an array of our ToDo.

Use our wrapper

Inside App.tsx we can finally call the GraphQL API with our wrapper:

import React, { useState, useEffect } from "react";
import { listTodos } from "./graphql/queries";
import { ListTodosQuery } from "./API";
import Todo, { mapListTodos } from "./models/todo";

// omitted Amplify.configure

function App() {
  const [todos, setTodos] = useState<Todo[]>();

  useEffect(() => {
    async function getData() {
      try {
        const todoData = await callGraphQL<ListTodosQuery>(listTodos);
        const todos = mapListTodos(todoData);
        setTodos(todos);
      } catch (error) {
        console.error("Error fetching todos", error);
      }
    }
    getData();
  }, []);

  return (
    <div className="App">
      {todos?.map((t) => (
        <div key={t.id}>
          <h2>{t.name}</h2>
          <p>{t.description}</p>
        </div>
      ))}
    </div>
  );
}

We create a state which contains Todos with the useState<Todo[]> Hook.
Then useEffect is used to call the API initially. Since the API call is asynchronous, an async function getData() is defined. This function uses our previsouly created wrapper callGraphQL() and defines the generic type as ListTodosQuery which is imported from the auto-generated API.ts. As argument listTodos is passed. This is the actual GraphQL query which is also auto-generated by Amplify. The result is passed to the mapListTodos function which will return the ToDos as an Array. Afterwards, the state is updated.

Create ToDo Mutation

To send a mutation the wrapper can be reused:

const name = 'Learn Amplify'
const description = 'Start first Amplify project'

const response = await callGraphQL<CreateTodoMutation>(createTodo, {
        input: { name, description },
      } as CreateTodoMutationVariables);

These types need to be imported:
CreateTodoMutation: Type of what the mutation will return
createTodo: GraphQL Mutation
CreateTodoMutationVariables: type of the argument that gets passed in. This is an object with an input property which is an object that contains the properties for our new ToDo.

Subscriptions

Subscriptions enable realtime updates. Whenever a new ToDo is created the subscription will emit the new ToDo. We can update the ToDo list with this new ToDo.

For that we create a generic interface SubscriptionValue:

interface SubscriptionValue<T> {
  value: { data: T };
}

We also need a new mapping function for our ToDo model:

function mapOnCreateTodoSubscription(createTodoSubscription: OnCreateTodoSubscription): Todo {
  const { id, name, description } = createTodoSubscription.onCreateTodo || {};
  return {
    id, name, description
  } as Todo
}

In App.tsx we add another useEffect which will handle the subscription:

import Todo, { mapOnCreateTodoSubscription } from './models/todo';
import { SubscriptionValue } from './models/graphql-api';
import { onCreateTodo } from './graphql/subscriptions';

useEffect(() => {
  // @ts-ignore
  const subscription = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
    next: (response: SubscriptionValue<OnCreateTodoSubscription>) => {
      const todo = mapOnCreateTodoSubscription(response.value.data);
      console.log(todo);
      setTodos([...todos, todo]);
    },
  });

  return () => subscription.unsubscribe();
});

This is probably the most difficult part of using the GraphQL API with TypeScript.
The Api.graphql(...) function return type is from Promise<GraphQLResult> | Observable<object>

Only the Observable has the subscribe function. Without the @ts-ignore the TypeScript compiler would complain that subscribe does not exist on type Promise<GraphQLResult> | Observable<object>.
Unfortunately, we can't just simply cast it via as Observable because the Amplify SDK does not export an Observable type. There is already a GitHub issues for that.

The subscribe function itself takes an object as an argument with a next property, which needs a function that gets called whenever a new ToDo is created (you can think of it as a callback).
The Parameter of that function is of type SubscriptionValue<OnCreateTodoSubscription. Pass response.value.data to the mapOnCreateTodoSubscription function which will return the ToDo. Afterwards, the state is updated with the new ToDo. Finally, in the return statement the subscription is unsubscribed when the component gets unmounted to avoid memory leak.

This may look a little verbose. This can be refactored to a wrapper function, as with the callGraphQL function:

function subscribeGraphQL<T>(subscription: any, callback: (value: T) => void) {
  //@ts-ignore
  return API.graphql(graphqlOperation(subscription)).subscribe({
    next: (response: SubscriptionValue<T>) => {
      callback(response.value.data);
    },
  });
}

This is again a generic function which will return the subscription. It accepts the subscription and a callback. The callback is called in the next handler and response.value.data is passed as the argument.

The useEffect with the Subscription can be refactored to this:

const onCreateTodoHandler = (
  createTodoSubscription: OnCreateTodoSubscription
) => {
  const todo = mapOnCreateTodoSubscription(createTodoSubscription);
  setTodos([...todos, todo]);
};

useEffect(() => {
  const subscription =
    subscribeGraphQL <
    OnCreateTodoSubscription >
    (onCreateTodo, onCreateTodoHandler);

  return () => subscription.unsubscribe();
}, [todos]);

The onCreateTodoHandler is responsible for calling the mapping function and updating the state with the new ToDo.
In useEffect we only call the new subscribeGraphQL wrapper function, passing the onCreateTodo subscription and our onCreateTodoHandler. As before, the subscription is unsubscribed when the components gets unmounted.

Summary

Amplify allows to scaffold a GraphQL API very quickly and even auto-generates TypeScript code for the frontend. With some wrapper functions the boilerplate code can be reduced and type-safety embraced.

The full source code is on GitHub

Feel free to leave a comment! :)

Top comments (3)

Collapse
 
adamguinea profile image
Adam Guinea

This is exactly what I needed! Thank you for the time you put into this.

Collapse
 
yeeiodev profile image
Pablo Yev

It is not as straight forward as I thought... really nice explained!! Do you have any project with Amplify on production?? I think I'll create my self one, but just to have it as a reference. Thanks!!

Collapse
 
rmuhlfeldner profile image
Roman Mühlfeldner Author

Thank you for your great feedback and sorry for the late response!
Unfortunately, I don't have any projects with Amplify on production.

Take a look at this:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠