DEV Community

Cover image for Building a Chat App with Fauna's GraphQL, Firebase, and Socket.io
Seun Taiwo
Seun Taiwo

Posted on

Building a Chat App with Fauna's GraphQL, Firebase, and Socket.io

If you've used Express to build a GraphQL server before, it might seem easy, but I'm here to show you an even easier way made by Fauna. Fauna's GraphQL takes away all the complexities of building a GraphQL server and sets it up with your schema ONLY. Isn't that awesome?

In this article, we will build a React Chat App using Firebase for authentication and connect to both a Socket IO server and Fauna's GraphQL server.

To understand this article, you would need to know React and Express, but I'll try my best to explain to them as we go. To follow along, you need to have the latest version of Node installed.

Sections

  1. Setting up Fauna's GraphQL.

  2. Setting up the React App with Firebase to handle Authentication.

  3. Setting up Socket.io on the client and the server.

  4. Conclusion and links to resources used in the article.

Setting up Fauna's GraphQL

First, head over to Fauna's website, create an account, which will direct you to the dashboard. If you're not referred to the dashboard right away, click here.

Fauna Dashboard

Click the new database button and enter your database name.

New Database Page

The 'Pre-populate with demo data' checkbox would be helpful if we wanted to autofill our database with some data, but we're creating ours, so you should leave it unchecked.

Head over to the Security Tab and click on New Key, then Save

Generating a New Key

You should be seeing your API Key now. Copy it somewhere as you'll need it.

API Key

This API Key is specific to this database, i.e., it won't work with other databases created on Fauna. All GraphQL queries going to Fauna must have an 'Authorization' header that contains your API key.

Create a folder to hold all files related to the server. I will call mine chat-app-API. You need to initialize your package manager. You can use npm or yarn, but I will be using yarn. From the terminal, run:

yarn init -y

This initializes your project with the default options.

We will need to install some dependencies to aid our development so run:

yarn add axios dotenv express socket.io & yarn -D fauna-gql-upload faunadb

  1. axios - It's a library used to make HTTP requests from the client or server.

  2. dotenv - It extracts our environmental variables from our .env file ( I'd explain what .env is in a bit ) and makes them available in our app.

  3. express - This library takes all the heavy lifting of setting up a server and handling requests in node.

  4. socket.io - This library allows us to enable the real-time connection between the client and the server.

  5. fauna-gql-upload - Removes the hassle of uploading our schema on Fauna's website or through the REST API, which might be a bit confusing. It helps us upload our GraphQL schema directly from the terminal. fauna-gql-upload is being installed as a Dev Dependency because it won’t be needed in production.

  6. fauna - This is a Fauna Javascript driver that enables us to work with their services. We're not using it directly, but the fauna-gql-upload package requires it.

Setting up the React App with Firebase to handle Authentication.

To get started, clone the react app from this GitHub repo. Open the terminal in the folder and run:

yarn install

To explain the React App briefly, I'm using ContextAPI for state management. When a user signs in, the user is stored and taken to the home screen. The home screen is where the users' chats and the list of users, a search button, and an input box are displayed. The search button searches for all users if the input box is empty when it is clicked. Otherwise, it will search for a user with the text entered into the input box. This page utilizes socket.io to get updated whenever a new chat is created. If any of the users are clicked, a new chat is created, and alerts are sent to all other clients about that chat. If a chat is clicked, the user is directed to the chat page where socket.io sends new messages to the recipient. To ensure consistency, I made sure the messages and chats were updated on the user whenever a new one was created. That's about it for the flow of the app.

To set up Firebase, all you need is a Google Account, after which you would create a new app by clicking the button and entering your app name. Go ahead and disable google analytics— we won't be needing it.

You should be on this page. Click Authentication, then Get Started from the subsequent page that loads.

Firebase Console

Firebase allows various authentication methods, but we will be using Google and Twitter—if you have an active Twitter Developer Account. I got my Twitter Developer Account verified in a day, so it shouldn't take too long. However, I would comment out the Twitter login button and its firebase config. If you do get the API KEYs, you can uncomment it from the firebase.utils.js and the login.component.jsx files.

Firebase Authentication Page

Activate Google Auth by clicking the pencil icon and following the prompt.

For Twitter Auth, you would need an API Key and API Secret. You would also need to set the Callback URL and Website URL of the application on your Twitter Developers' Dashboard. To get the URLs, head back to your Firebase console and click on the gear icon beside Project Overview, and from there, you may click on Project Settings.

Project Settings

Callback URL - https://[projectid].firebaseapp.com/__/auth/handler
Website URL - https://[project-id].firebaseapp.com

Scroll to the bottom of the page and click on this:

Registering a Firebase Web App

Register the Web App without Firebase Hosting. You will get this, which holds vital information that enables us to connect our React App to Firebase:

Firebase Config

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
};

firebase.initializeApp(firebaseConfig);
Enter fullscreen mode Exit fullscreen mode

The above code is in the repo at chat-app/src/firebase/firebase.utils.js where we initialize our Firebase App. If you're wondering what process.env is and looking for where we defined it, well, we didn't. process.env is where Node stores our environment variables. What are environment variables? I like to see environment variables as external variables that we don't set directly in our code; instead, they are determined by the OS. They're essential for two main reasons:

  1. Security: We won't want to expose vital information about our Firebase App by putting it on the client-side, which is open for everyone to see.
  2. Easily changeable: Say you've deployed a web app that uses an API Key in about five different files in your codebase. You would need to go to your codebase and start making the changes in the different files. With environment variables, you have to go to your deployment platform and change the environment variables.

While developing, we store these environment variables in a .env file and add them to our .gitignore file if we're using git, so they don't get committed. A typical .env looks like so:

HELLO_WORLD="bar"
HI="foo"
Enter fullscreen mode Exit fullscreen mode

The variable names are always in uppercase with spaces as underscores (Constant Case), and the values are between quotes. Read more about environment variables here.

Our .env would therefore look like this:

REACT_APP_API_KEY=""
REACT_APP_AUTH_DOMAIN=""
REACT_APP_PROJECT_ID=""
REACT_APP_STORAGE_BUCKET=""
REACT_APP_MESSAGING_SENDER_ID=""
REACT_APP_APP_ID=""
REACT_APP_FAUNA_SECRET=""
Enter fullscreen mode Exit fullscreen mode

Add the Firebase config values and your Fauna Secret, which we created earlier, to your .env file.

Back to the cream of the crop: Fauna. Copy the schema as written below. If you're not familiar with GraphQL, your schema tells your GraphQL server exactly how you want it to be structured, i.e., the possible queries that can be made and the possible mutations that can be made, as well as their arguments and their results. They must all be appropriately specified.

Setting up Socket.io on the client and the server.

We're using socket.io to alert other users when a new chat has been created or a new message sent. When a chat is created, an API request is sent to the server to create that chat, which sends a request to Fauna. If successful, all users are notified, but only the two users involved in the chat get a UI update. For a message, the same process occurs, except if successful, the message is sent to the two users of the chat alone.

On the client ( your React App ), socket.io has already been set up. I use socket.io in the home.component.jsx and chat.component.jsx files to enable the real-time connection. You can check those files to see how I implemented them. In the chat-app-API folder we created in step 1, add this code to a file named app.js:

require("dotenv").config();
const PORT = process.env.PORT || 3000;
const express = require("express");
const axios = require("axios");
const app = express();
const httpServer = require("http").createServer(app);

app.use(express.json());

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "OPTIONS, GET, POST, PUT, PATCH, DELETE"
  );
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  next();
});

app.post("/chat", async (req, res) => {
  const { user1Id, user2Id } = req.body;
  if (!user1Id || !user2Id) {
    console.log("IDs missing for chat");
    return res.status(400).send({
      status: "Failed",
      message: "Send both IDs to create a chat",
    });
  }
  const body = {
    query: `
    mutation CreateChat($user1: ID, $user2: ID){
      createChat(data:{
        users:{
          connect:[$user1,$user2]
        }
      }){
        _id
        messages{
          data{
            content
            sender{
              _id
            }
          }
        }
        users{
          data{
            _id
            name
            image
          }
        }
      }
    }
    `,
    variables: {
      user1: user1Id,
      user2: user2Id,
    },
  };
  try {
    const response = await axios.post(
      "https://graphql.fauna.com/graphql",
      body,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.FGU_SECRET}`,
        },
      }
    );
    console.log(response.data);
    io.emit("newChat", response.data.data.createChat);
    res.send({
      status: "Successful",
      message: "Chat Saved Successfully",
    });
  } catch (e) {
    console.log(e);
  }
});

app.post("/message", async (req, res) => {
  console.log(req.url);
  const { message, chatID } = req.body;
  const body = {
    query: `
    mutation CreateMessage($chatID: ID, $senderID: ID, $content: String!){
      createMessage(data:{
       chat:{
         connect: $chatID 
       }
       content: $content
       sender: {
         connect: $senderID
       }
     }){
       content
       _ts
       sender{
         name
         _id
       }
     }
   }
    `,
    variables: {
      chatID,
      senderID: message.senderID,
      content: message.content,
    },
  };
  try {
    const response = await axios.post(
      "https://graphql.fauna.com/graphql",
      body,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.FGU_SECRET}`,
        },
      }
    );
    console.log(response.data);
    console.log(chatID);
    io.sockets.in(chatID).emit("newMessage", response.data.data.createMessage);
    res.send({
      status: "Successful",
      message: "Received",
    });
  } catch (e) {
    console.log(e);
  }
});
const io = require("socket.io")(httpServer, {
  cors: {
    origin: "*",
  },
});
io.on("connection", (socket) => {
  const { chatId } = socket.handshake.query;
  socket.join(chatId);
  console.log(`Connected to ID ${socket.id}`);
});

httpServer.listen(PORT, () => {
  console.log(`Server Started on Port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

In the code above, we added two middlewares, the first for parsing our request bodies and the second for setting our headers to prevent CORS errors. We then implemented our /chat and /message routes for adding chats and messages, respectively. We finally initialized our socket.io connection and turned on the server by calling the listen function.

Lastly, we need our Fauna GraphQL schema. Create a folder named fauna in the chat-app-API folder and a file named schema.gql, and add the following code to it:

type User {
  name: String! @unique
  image: String
  chats: [Chat] @relation
}

type Chat {
  users: [User!]! @relation
  messages: [Message] @relation
}

type Message {
  chat: Chat!
  content: String!
  sender: User!
}

type Query {
  allUsers: [User!]!
  allChats: [Chat!]!
  allMessages: [Message!]!
  findUserByName(name: String!): User
}
Enter fullscreen mode Exit fullscreen mode

Remember, we installed a nifty little tool to upload our schema. We use it here, but let's add it to our package.json script for ease of use. Add this line to the scripts object:

"fauna": "fgu"

For the tool to work, we need to add our Fauna Secret here too hence we definitely need another .env file. Create one and add the following with your secret.

FGU_SECRET=""

When this is done, run

yarn fauna

or

npm run fauna

depending on your package manager and you should get:

Uploading Schema

If that didn't work, return to your Fauna Dashboard, go to the GraphQL tab, and import the schema yourself.

At this point, you can start your server as well as your React app on two different browsers and see your Chat App working flawlessly.

You can reach me on Twitter at @the_dro_ if you have any questions about this article. If you'd love to learn more about Fauna, you can go to their documentation or contact me as well. Israel did the cover art of this article.

Thank You.

Top comments (0)