Build a Multi-User Chat App with AWS Amplify

Christian Nwamba - Jun 13 '23 - - Dev Community

In today's world, technology touches practically every part of our lives. We depend significantly on applications and platforms that enable smooth and effective communication, and chat apps are an essential component of this digital ecosystem. Chat apps like WhatsApp, Slack, and Teams have transformed communication and collaboration. They have become vital for staying in touch with friends and family, discussing projects with coworkers, and even networking with professionals in various areas of our lives.

As a developer, if you want to build a multi-user Chat app, AWS Amplify is a great solution that streamlines the development process efficiently. AWS Amplify is a comprehensive development platform that provides various services such as authentication, APIs, storage, and more, making it easier to build sophisticated applications. Amplify offers a managed GraphQL service, a robust API technology that allows clients to request exactly the data they need and real-time updates on that data.

In this article, we'll look at how to build a real-time chat application using React and AWS Amplify GraphQL API, a part of Amazon's cloud services that allows developers to build and deploy secure, scalable cloud apps.

Prerequisites

To follow this tutorial, you'll need:

  1. An AWS account. If you don't have one, you can create an account here.
  2. Node.js v14.x or later and npm v6.14.4 or later

This tutorial also assumes that you're familiar with the basics of both JavaScript/ES6 and React.

Set up Your Development Environment

You need to configure the Amplify CLI locally, and connect it to your AWS account. Run the following command in your terminal:



npm install -g @aws-amplify/cli


Enter fullscreen mode Exit fullscreen mode

Next, run the amplify configure command to configure your AWS Amplify environment. Follow the prompts to create a new IAM user that AWS Amplify will use to manage resources for your app. Here’s a link with step-by-step details on how to configure the Amplify CLI.

Create an AWS Amplify Project

The Amplify Console is where you build, manage, and host your Amplify applications. Let's set up an AWS Amplify app. Log into your AWS account and search for AWS Amplify in the console. Select AWS Amplify to open the Amplify Console.

If this is your first app, scroll to the bottom of the page in the Amplify Studio section, select Get started. Name your app, and select Confirm Deployment.

If you’ve created an Amplify App in the past, follow the steps below:

  1. Select New app in the upper right-hand corner, and select Build an app from the dropdown menu.

  1. Name your app, and select Confirm Deployment.

This would take a few seconds as Amplify need some time to set up a new project. Once that is completed, click the Launch Studio button.

AWS Amplify Studio is a visual development environment tailored for building fullstack web and mobile apps. One of its standout features is the ease with which you can set up backend resources for various tasks, such as authentication and managing customer data.

It also has a Content Management System (CMS), which is incredibly useful for viewing and manipulating user data.

You should now see the Home menu for our application. To learn more, see the Amplify Studio introduction.

Set up Authentication

We need to set up authentication for our app so that users can create accounts and sign in, and only authenticated users can use the app.

Writing the code for an application's login flow can be difficult and time-consuming. In Amplify Studio, you can easily add a complete Amazon Cognito authentication solution to your app. When you specify the login method, you will be provided with the Authenticator UI component for the entire authentication flow.

As seen in the video below:

  1. Select the Authentication option in the setup menu on the screen's left side. You don't need to choose a login method in the Configure login section, as Email is already selected as the default option.

  2. Scroll down to the Configure Signup settings section. You should see the Password protection settings in this section. Click on this to see a variety of security options for user passwords.

  3. Click the Deploy button*,* acknowledge the warning, and select

Confirm Deployment.

You should see the progress of this deployment process — a visual representation of how your setup is being deployed in the AWS environment.

This process should take a bit of time, but when it is done, you should see a confirmation message saying that you’ve successfully deployed authentication.

Create Data Model

Next, we need to define a data model for the chat app to store the chat messages. One of the features that Amplify provides is the ability to create data models, which represent the structure of the data your application will work with. The Studio data model designer provides a visual way to achieve this.

When you define your data models, AWS Amplify creates corresponding AWS AppSync GraphQL APIs to allow your application to interact with the data and Amazon DynamoDB tables based on those models.

Follow the steps below to create a data model for your app:

  1. On the Setup menu on the left, select Data, and select Add model.

  1. As seen in the image below, we call our data model Chat for this example. Click the Add a field link, and in the field that appears, type in "text".

  2. For this text field, you will notice an option labeled 'is required' to the right. Check this box to make this a mandatory field. Add another field to the data model, and for this, type in "email".

  3. Once you have completed these steps, find and select the Save and Deploy button. Acknowledge the warning, and select Deploy.

The deployment should take a few minutes, but once it is completed, you should see a message saying you've successfully deployed the data model.

Additionally, you should also see a "pull" command that should be executed in your local terminal, precisely from your project's root directory to pull the project into your React project. You can either copy the command or ignore it for now; we'll use it soon. Let’s go ahead and set up our React project.

Set up React Project

To keep the focus of this guide on building our chat app, I'll skip the steps in setting up certain dependencies, such as Tailwind CSS for styling and date-fns for date formatting.

Run the following command from your preferred directory to clone a React starter project that already includes these dependencies.



git clone https://github.com/ifeoma-imoh/amplify-chatty-starter.git


Enter fullscreen mode Exit fullscreen mode

Run the following command to navigate into the realtime-chat-app directory, install the dependencies, and start up your development server:



cd realtime-chat-app
npm install
npm start


Enter fullscreen mode Exit fullscreen mode

This lets you see the output generated by the build. You can see the running app by navigating to http://localhost:3000.

Navigate to your Amplify Studio application, and copy the pull command displayed. This command is important as it allows you to pull the newly created Amplify backend project from the cloud to your local environment (React project in our case).

Dependency: Make sure you have AmplifyCLI configured before executing the pull command.

From your projects directory, paste the copied command into the terminal and then execute it. A typical instance of this command would appear as follows:



amplify pull --appId d2act21onzf92o --envName staging


Enter fullscreen mode Exit fullscreen mode

The appId specifies the unique ID of the Amplify App you want to pull and envName staging specifies the environment of the Amplify App that you want to pull. An Amplify App can have multiple environments (like 'development', 'staging', 'and production').

When you execute the command, you will be automatically redirected to your web browser to grant authorization. Once there, click 'Yes' to authenticate with Amplify Studio.

After successful login, return to the CLI. Here, you will be asked a series of questions to gather essential details about your project's configuration. Your answers will help Amplify understand your project's structure and needs.

Accept the default values highlighted below:

The Amplify CLI will automatically carry out the following steps for you:

  1. Amplify will create a new project in the current local directory. This is indicated by the creation of an amplify folder in your project's root directory, which contains all of your project's backend infrastructure settings.
  2. It creates a file called aws-exports.js in the src directory of your React application. It holds all the configuration for the services you create with Amplify, allowing the Amplify client to access necessary information about your backend services, such as API endpoints, authentication configuration, etc.

Next, we need to generate the necessary GraphQL operations (queries, mutations, subscriptions) based on our schema that will be used to interact with our API.

Run the following command at the root of your project:



amplify codegen


Enter fullscreen mode Exit fullscreen mode

This command will generate the necessary GraphQL operations and save them in a graphql directory in src/graphql directory. These operations are ready-to-use and can be imported into your React components to interact with your API.

Install Amplify Libraries

The first step to using Amplify in the client is to install the necessary dependencies:

Run the following command to install the Amplify Libraries:



npm install aws-amplify @aws-amplify/ui-react


Enter fullscreen mode Exit fullscreen mode

The aws-amplify package is the library that enables you to connect to your backend resources. The @aws-amplify/ui-react package includes React-specific UI components that you can use to build your app UI.

We installed the Amplify UI package to use the Amplify Authenticator-connected UI components. It provides the entire authentication flow using the configuration specified in your **aws-exports.js** file.

Configure Amplify

For your application to communicate and interact with the services provided by AWS, we need to configure AWS Amplify for use throughout the project.

In your app's entry point index.js, import and load the configuration file:



import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

// Add these lines
import { Amplify } from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);


Enter fullscreen mode Exit fullscreen mode

In the code above, we import the Amplify library and configuration details from the aws-exports.js file.

We then configure AWS Amplify with the details we imported. This setup ensures that the Amplify library knows about your specific backend setup and can interact with it correctly.

Protect Your App UI with Amplify Auth

One way to add authentication capabilities to our application is by using the Amplify authentication UI components, which provide the entire authentication flow for us, using your configuration specified in our aws-exports.js file.

Open your **src/App.js** file and import the withAuthenticator component below the last import.



import "@aws-amplify/ui-react/styles.css";
import { withAuthenticator } from "@aws-amplify/ui-react";


Enter fullscreen mode Exit fullscreen mode

Here, we import the withAuthenticator higher-order component from the AWS Amplify UI React library. This component automatically provides a pre-built sign-in and sign-up interface for your app and the associated functionality. The first line imports the default styling for these UI components.

Next, update the code in your **src/App.js** file to match the following:



import React from "react";
//...

function App({ user, signOut }) {
  return (
    <div>
      <div className="flex justify-end px-4 py-2">
        <button
          type="button"
          className="relative inline-flex items-center gap-x-1.5 rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
          onClick={() => signOut()}
        >
          Sign Out
        </button>
      </div>
      <div className="flex justify-center items-center h-screen w-full">
        Hello World
      </div>
    </div>
  );
}

export default withAuthenticator(App);


Enter fullscreen mode Exit fullscreen mode

For now, The App function is a simple React component that displays a greeting message and a sign-out button. It receives signOut and user props. The signOut function is used to log the user out of the application, and user contains the details of the currently logged-in user.

Next, we wrap the App component with the withAuthenticator higher-order component, thereby adding authentication functionality to the app. When the App component is exported in this way, only authenticated users can access the app’s UI.

Now if you head over to your browser, you should see a default sign-in/sign-up UI for users that are not authenticated.

Use this form to create and authenticate a new account to access the app. After a successful sign-in, you should see the user logged to the console.

Display User Chat Messages

Let's create a text input field that gives immediate feedback by displaying any text typed into it on the screen when the user hits the 'Enter' key. Update the code in your **src/App.js** file to match the following:



function App({ user, signOut }) {
  const [chats, setChats] = React.useState([]);
  // console.log(user);
  return (
    <div>
      <div className="flex justify-end px-4 py-2">
        <button
          type="button"
          className="relative inline-flex items-center gap-x-1.5 rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
          onClick={() => signOut()}
        >
          Sign Out
        </button>
      </div>
      <div className="flex justify-center items-center h-screen w-full">
        <div className={`w-3/4 flex flex-col`}>
          {chats}
          <div>
            <div className="relative mt-2 flex items-center">
              <input
                type="text"
                name="search"
                id="search"
                onKeyUp={async (e) => {
                  if (e.key === "Enter") {
                    setChats([...chats, e.target.value]);
                  }
                }}
                className="block w-full rounded-md border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              />
              <div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
                <kbd className="inline-flex items-center rounded border border-gray-200 px-1 font-sans text-xs text-gray-400">
                  Enter
                </kbd>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default withAuthenticator(App);


Enter fullscreen mode Exit fullscreen mode

In the code above, the App component renders a text input for creating new chats. We created a state variable called chats that will be used to store the chat messages we will fetch from the GraphQL API.

For now, when a user enters a text and hits the Enter key on the keyboard, the value of the input field is passed into the setChats function to update the local state.

Right now, when the user types a message and hits Enter, the message is displayed on the screen immediately. This is just a placeholder functionality we have put in place for now.

Next, we will work on fetching and displaying real-time messages using the GraphQL API for a complete chat experience.

Store the Chat Messages

Let’s create GraphQL mutations using the user's input to create new chat messages. Update the code in your **src/App.js** file to match the following:



//...

// Import these
import { API } from "aws-amplify";
import * as mutations from "./graphql/mutations";

function App({ user, signOut }) {
  const [chats, setChats] = React.useState([]);
  return (
    <div>
      <div className="flex justify-end px-4 py-2">
        <button
          type="button"
          className="relative inline-flex items-center gap-x-1.5 rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
          onClick={() => signOut()}
        >
          Sign Out
        </button>
      </div>
      <div className="flex justify-center items-center h-screen w-full">
        <div className={`w-3/4 flex flex-col`}>
          {chats}
          <div>
            <div className="relative mt-2 flex items-center">
              <input
                type="text"
                name="search"
                id="search"
                onKeyUp={async (e) => {
                  if (e.key === "Enter") {
                    // Remove this line
                    // setChats(e.target.value);

                    // Add these
                    await API.graphql({
                      query: mutations.createChat,
                      variables: {
                        input: {
                          text: e.target.value,
                          email: user.attributes.email,
                        },
                      },
                    });
                    e.target.value = "";
                  }
                }}
                className="block w-full rounded-md border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              />
              <div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
                <kbd className="inline-flex items-center rounded border border-gray-200 px-1 font-sans text-xs text-gray-400">
                  Enter
                </kbd>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default withAuthenticator(App);


Enter fullscreen mode Exit fullscreen mode

In the code above, we import API from the AWS Amplify library, which provides the ability to interact with the GraphQL API. It also imports mutations from the graphql/mutations file.

When the user types something in the input field and presses Enter, it triggers the onKeyUp event. The function it receives sends a GraphQL mutation request to create a new chat.

The mutation request is made using API.graphql, and we’re passing it an object that includes our mutation and the input variables. query refers to the specific GraphQL mutation that should be made (in this case, mutations.createChat defined in the imported mutations file). variables is an object that provides the input data for the mutation (the chat text and the user's email). The structure of this input matches what our mutation expects according to our GraphQL schema.

After the mutation request, we clear the input field by setting e.target.value to an empty string.

Type in new messages and hit the “Enter” key on you keyboard. To view them, navigate to the Amplify Studio dashboard for your application. Inside Amplify Studio, locate the "Manage" section in the sidebar on the left. Within this section, select "Content". The chat messages are stored in a DynamoDB database managed by AWS Amplify.

Fetch the Chat Messages

Now, we are set to fetch and display all the chat messages stored in our Amplify database using a GraphQL query. Update the code in your **src/App.js** file to match the following:



//...

import { API } from "aws-amplify";
import * as mutations from "./graphql/mutations";

// Import these
import * as queries from "./graphql/queries";
import intlFormatDistance from "date-fns/intlFormatDistance";

function App({ user, signOut }) {
  const [chats, setChats] = React.useState([]);

  // Add these
  React.useEffect(() => {
    async function fetchChats() {
      const allChats = await API.graphql({
        query: queries.listChats,
      });
      console.log(allChats.data.listChats.items);
      setChats(allChats.data.listChats.items);
    }
    fetchChats();
  }, []);

  return (
    <div>
      //...
      <div className="flex justify-center items-center h-screen w-full">
        <div className={`w-3/4 flex flex-col`}>
          {/* Add these */}
          {chats
            .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
            .map((chat) => (
              <div
                key={chat.id}
                className={`flex-auto rounded-md p-3 ring-1 ring-inset ring-gray-200 w-3/4 my-2 ${
                  chat.email === user.attributes.email && "self-end bg-gray-200"
                }`}
              >
                <div>
                  <div className="flex justify-between gap-x-4">
                    <div className="py-0.5 text-xs leading-5 text-gray-500">
                      <span className="font-medium text-gray-900">
                        {chat.email.split("@")[0]}
                      </span>{" "}
                    </div>
                    <time
                      dateTime="2023-01-23T15:56"
                      className="flex-none py-0.5 text-xs leading-5 text-gray-500"
                    >
                      {intlFormatDistance(new Date(chat.createdAt), new Date())}
                    </time>
                  </div>
                  <p className="text-sm leading-6 text-gray-500">{chat.text}</p>
                </div>
              </div>
            ))}

          <div>
            <div className="relative mt-2 flex items-center">//...</div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default withAuthenticator(App);


Enter fullscreen mode Exit fullscreen mode

At the top of the file, we import queries from the graphql/queries file, which was auto-generated by the CLI. In the App component, we define a useEffect hook. This hook runs the provided function — fetchChats, after the first render of the component, and every time the values in the dependency array change.

The fetchChats function fetches all chat items by making a GraphQL query request using the API.graphql method and the listChats query. The result is an array of chat objects which we log to the console and update the chats state variable with the fetched chats.

In the JSX returned by the component, we map over the chats array to create a list of chats, each representing a chat message.

The chats are sorted by creation time to ensure the most recent chats appear last. We displayed the user's email for each chat but only the part before the "@" sign. We displayed the time when the chat was created using the intlFormatDistance function (imported from the date-fns library) to format the time as a relative time string. We also visually distinguish chats sent by the current user using a different background color.

Refresh your browser now, and you should see the chat messages displayed nicely as seen below.

If you type in a new chat message, you’d have to manually refresh your browser to see it displayed on the screen. Next up, we need to set up a mechanism for updating our app in real-time.

Subscribe to Data

Subscriptions are a part of the GraphQL spec, where the server can actively update its clients when certain events occur. It is a way to incorporate real-time data flow into your application, keeping it updated without needing constant manual refreshes like we did in the previous section. It is like having a direct channel between the server and your app where the server constantly notifies your app about any changes that you have specified as significant.

Update the code in your **src/App.js** file to match the following:



//...

// Import graphqlOperation
import { API, graphqlOperation } from "aws-amplify";

//...

//Import this
import * as subscriptions from "./graphql/subscriptions";
function App({ user, signOut }) {
  const [chats, setChats] = React.useState([]);

  //...

  // Add this
  React.useEffect(() => {
    const sub = API.graphql(
      graphqlOperation(subscriptions.onCreateChat)
    ).subscribe({
      next: ({ provider, value }) =>
        setChats((prev) => [...prev, value.data.onCreateChat]),
      error: (err) => console.log(err),
    });
    return () => sub.unsubscribe();
  }, []);

  return (
    <div>
      <div className="flex justify-end px-4 py-2">//...</div>di
    </div>
  );
}

export default withAuthenticator(App);


Enter fullscreen mode Exit fullscreen mode

In the code above, we import the necessary dependencies: graphqlOperation from AWS Amplify, which allows us to build a subscription GraphQL query and our GraphQL subscriptions from a local file.

Then, we define a new useEffect hook, which is subscribing to a GraphQL subscription.
Within this hook, we subscribe to the onCreateChat subscription. When a new chat is created, our GraphQL server will send this data to our client.

The subscribe function takes an object with up to three properties: next, error, and complete. In our case, we only use next and error. next is a function that will run whenever new data is sent from our server. Here, it's updating our chats state with the new chat message. error is a function that will run if errors occur when setting up the subscription or receiving data.

Finally, the hook returns a cleanup function, which will run when our component is unmounted. This function unsubscribes from the GraphQL subscription to clean up potential memory leaks.

If you head over to your browser and type in a chat message now, you should see it displayed on the screen immediately.

Go ahead and test the app with multiple users by creating a new account on a different browser.

Clean Up

To ensure that you don’t have any unused resources in you AWS account, run the following command to delete all the resources that were created in this project if you don’t intend to keep them.



amplify delete

Enter fullscreen mode Exit fullscreen mode




Conclusion

From creating data using GraphQL mutations to fetching and displaying it in a user-friendly interface, we've explored how to effectively leverage AWS Amplify's powerful capabilities to build a chat application.

This demonstrates the strength and flexibility of AWS Amplify. While we've focused on building a chat application, the techniques we've covered have much more to offer and can be applied to various projects.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .