How I Built a Command-Line Chat Application: The Client Code Explained
Published: 2023-06-08 21:12:36

---
title: How I Built a Command-Line Chat Application: The Client Code Explained
series: How I Built a Command-Line Chat Application
published: true
date: 2023-06-07 22:17:41 UTC
tags: 
canonical_url: https://hayatscodes.hashnode.dev/how-i-built-a-command-line-chat-application-the-client-code-explained
---

<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->

- [Client Authentication](#client-authentication)
    + [User Registration](#user-registration)
    + [User Login](#user-login)
    + [User Token](#user-token)
- [Home Menu](#home-menu)
    + [Creating Chat Rooms](#creating-chat-rooms)
    + [Joining Chat Rooms](#joining-chat-rooms)
    + [Exiting the App](#exiting-the-app)
- [User Interface](#user-interface)
    + [Authentication Display Options](#authentication-display-options)
    + [Home Display Options](#home-display-options)
    + [Interface Rendering](#interface-rendering)
    + [Chat Message Interface](#chat-message-interface)
- [The Socket Client and Node Script](#the-socket-client-and-node-script)
    + [Socket Client Events](#socket-client-events)
    + [The Node Script](#the-node-script)

<!-- TOC end -->

In this section, We'll dive into the client-side code implementation. The chat application provides users with the ability to register, log in, create chat rooms, join existing chat rooms, and exchange messages with other users. We will dissect the client code, examining the authentication logic, user registration and login functionalities, as well as the menu options for creating and joining chat rooms. Additionally, we will explore the user interface rendering and the event handling using the socket.io-client library.

<a name="client-authentication"></a>
## Client Authentication

Starting from the client's `/src` folder, the `/auth` folder contains the client's authentication logic. It has three files: `registerUser.js`, `loginUser.js`, and `getToken.js`.

<a name="user-registration"></a>
#### User Registration

```
// registerUser.js
const { prompt } = require('inquirer');
const axios = require('axios');

const loginUser = require('./loginUser');

const registerUser = async () => {
  const questions = [
    {
      type: 'input',
      name: 'username',
      message: 'Enter your username:',
    },
    {
      type: 'input',
      name: 'email',
      message: 'Enter your email:',
    },
    {
      type: 'password',
      name: 'password',
      message: 'Enter your password:',
    },
  ];

  try {
    const answers = await prompt(questions);
    const { username, email, password } = answers;

    const response = await axios.post('https://terminal-chat-app-production.up.railway.app/auth/register', {
      username,
      email,
      password,
    });

    console.info(response.data.message); // Registration successful
    console.info('-----------------------');
    const token = loginUser(username, password, email);
    return token;
  } catch (error) {
    if ( error.response.data.message === 'Username or email already exists') {
      console.info(error.response.data.message);
      registerUser();
    } else {
      console.error(error.response.data);
    }
  }
};

module.exports = registerUser;

```

`registerUser.js` prompts users with a list of questions asking for their details. The answers are then sent in a post request to the server's registration endpoint.

After successful registration, the user is logged in with the registered details (we'll take a look at that in the next heading). A token is then retrieved from the login operation, which is then returned from the function.

Also, if a user enters an existing email or password, they are prompted again to enter their registration details.

<a name="user-login"></a>
#### User Login

```
// loginUser.js
const axios = require('axios');
const { prompt } = require('inquirer');

const loginUser = async (username, password, email = null) => {
  if (email) {
    try {
      const response = await axios.post('https://terminal-chat-app-production.up.railway.app/auth/login', {
        username,
        password,
      });

      const token = response.data.token;

      console.log(response.data.message); // login successful
      return token;

    } catch (error) {
      console.error(error.response.data); // login error
    }
  } else {
    const questions = [
      {
        type: 'input',
        name: 'username',
        message: 'Enter your username:',
      },
      {
        type: 'password',
        name: 'password',
        message: 'Enter your password:',
      },
    ];

    try {
      const answers = await prompt(questions);
      const { username, password } = answers;

      const response = await axios.post('https://terminal-chat-app-production.up.railway.app/auth/login', {
        username,
        password,
      });

      const token = response.data.token;
      console.log(response.data.message); // login successful
      return token;
    } catch (error) {
      if (error.response.data.message === 'Invalid username or password') {
        console.info(error.response.data.message);
        loginUser(username, password);
      } else {
        console.error(error.response.data); 
      }
    }
  }
};

module.exports = loginUser;

```

`loginUser.js` exports a `loginUser` function that takes three parameters, with the `email` parameter initially set to null.

Since the user's email is available during registration, it is used to handle user login immediately after registration. The `username` and `password` parameters are then used to quickly make a request to the server's login endpoint.

However, when the email remains null during normal login, the user is prompted to enter their `username` and `password`, which are then used to make a request to the server's login endpoint.

The user is also prompted again to enter their login details when invalid credentials are entered.

<a name="user-token"></a>
#### User Token

```
// getToken.js
const axios = require('axios');

module.exports = async (username) => {
    try {
        const response = await axios.get(`https://terminal-chat-app-production.up.railway.app/auth/tokens/${username}`);
        const token = response.data
        return token;
    } catch(error) {
        console.error(error.response.data); 
    }

}

```

`getToken.js` simply gets the user token with their username from the server's running Redis database.

<a name="home-menu"></a>
## Home Menu

Next, we move into the `/menu` folder. It contains the logic for the home menu options. There are three folders in it: `createChatRoom.js`, `joinChatRoom.js`, and `exitApp.js`.

<a name="creating-chat-rooms"></a>
#### Creating Chat Rooms

```
// createChatRoom.js
const { prompt } = require('inquirer');
const joinChatRoom = require('./joinChatRoom');
const axios = require('axios');
const question = [
  {
    type: 'input',
    name: 'roomName',
    message: 'Enter Room Name'
  }
]

module.exports = async function createChatRoom(client) {
  try {
    const answer = await prompt(question);
    const roomName = answer.roomName;

    const response = await axios.post('https://terminal-chat-app-production.up.railway.app/api/chatrooms', {
      roomName
    });
    const chatRoom = response.data;
    console.log(`${chatRoom} chat room created`);
    joinChatRoom(client, chatRoom);
    return chatRoom;
  } catch (error) {
    if (error.response.data.message) {
      console.info(error.response.data.message);
      createChatRoom(client); // Recursive call to prompt again
    } else {
      console.error(error);
    }
  }
};

```

`createChatRoom.js` exports a function that takes a single parameter, `client`.

It starts by prompting the user to enter a room name. The room name is then sent in a `POST` request to the server to store it in the database.

The user then joins the room created by passing `client`, which is a socket-client instance, and the room name to the `joinChatRoom` function (which will be explained later). The created chat room is then returned from the function.

If an error occurs, the user is prompted to create the chat room again.

<a name="joining-chat-rooms"></a>
#### Joining Chat Rooms

```
// joinChatRoom.js
const { prompt } = require('inquirer');
const axios = require('axios');

module.exports = async function joinChatRoom(client, chatRoom = null) {
    if (chatRoom) {
        client.emit('join', chatRoom);
    } else {
        const response = await axios.get('https://terminal-chat-app-production.up.railway.app/api/chatrooms');
        const chatRooms = response.data;
        const chatRoomsOption = [
            {
                type: 'list',
                name: 'selectedRoom',
                message: 'Choose a Chat Room:',
                choices: chatRooms,
            },
        ]
        const { selectedRoom } = await prompt(chatRoomsOption);
        client.emit('join', selectedRoom);
        return selectedRoom;
    }
}

```

`joinChatRoom.js` exports a function that takes two parameters: `client`, a `socket.io-client` instance, and `chatRoom`, which is set to null.

It triggers the `join` event immediately when the `chatRoom` is not null. This happens when a user finishes creating a new chat room in `createChatRoom.js`.

Also, if a user wants to join an existing room, the user is prompted to choose a room from the list of rooms retrieved from the server. The `join` event is then triggered, with the selected chat room sent to the `socket.io` server. The selected chat room is then returned from the function.

<a name="exiting-the-app"></a>
#### Exiting the App

```
// exitApp.js
module.exports = () => {
    console.info('Exited Terminal Chat App Successfully!');
    process.exit(0);
}

```

`exitApp.js` exports an anonymous function that simply performs a process termination operation. This terminates the app, and a message is logged on to the user's screen.

<a name="user-interface"></a>
## User Interface

Into the `/views` folder, there is the user interface logic. It consists of four files: `getAuthOption.js`, `getMenuOption.js`, `renderInterface.js`, and `chatMessageInterface.js`.

<a name="authentication-display-options"></a>
#### Authentication Display Options

```
// getAuthOptions.js
const { prompt } = require('inquirer');

function getAuthOption() {
  const authOptions = [
    {
      type: 'list',
      name: 'selectedOption',
      message: 'Authentication',
      choices: [
        { name: 'Register', value: 'Register', message: 'Create an account' },
        { name: 'Login', value: 'Login', message: 'Login to your account' },
        { name: 'Exit', value: 'Exit', message: 'Exit the App' },
      ]
    },
  ];

  return prompt(authOptions)
    .then(answers => answers.selectedOption);
}

module.exports = getAuthOption;

```

`getAuthOption.js` exports a function that prompts users to select an authentication option of either `Register`, `Login`, or `Exit`. It then returns the selected option.

<a name="home-display-options"></a>
#### Home Display Options

```
// getMenuOption.js
const { prompt } = require('inquirer');

function getMenuOption() {
  const menuOptions = [
    {
      type: 'list',
      name: 'selectedOption',
      message: 'Home',
      choices: [
        { name: 'Create-Chat-Room', value: 'Create-Chat-Room', message: 'Create a Chat Room' },
        { name: 'Join-Chat-Room', value: 'Join-Chat-Room', message: 'Join a Chat Room' },
        { name: 'Exit', value: 'Exit', message: 'Exit the App' },
      ]
    },
  ];

  return prompt(menuOptions)
    .then(answers => answers.selectedOption);
}

module.exports = getMenuOption;

```

`getMenuOption.js` exports a function that prompts users to select a home menu option of either `Create-Chat-Room`, `Join-Chat-Room`, or `Exit`. It then returns the selected option.

<a name="interface-rendering"></a>
#### Interface Rendering

```
// renderInterface.js
const registerUser = require('../auth/registerUser');
const loginUser = require('../auth/loginUser');
const createChatRoom = require('../menu/createChatRoom');
const joinChatRoom = require('../menu/joinChatRoom');
const exitApp = require('../menu/exitApp');

const render = {
    'Register': registerUser,
    'Login': loginUser,
    'Create-Chat-Room': createChatRoom,
    'Join-Chat-Room': joinChatRoom,
    'Exit': exitApp
};

module.exports = render;

```

`renderInterface.js` exports a render object that stores the authentication and home menu options functions.

<a name="chat-message-interface"></a>
#### Chat Message Interface

```
// chatMessageInterface.js
const readline = require('readline');
const io = require('socket.io-client');
const exitApp = require('../menu/exitApp');
const getMenuOption = require('./getMenuOption');
const render = require('./renderInterface');
const getToken = require('../auth/getToken');
const attachEvents = require('../../attachEvents');

function chatMessageInterface(client, chatRoom) {
  console.info('----------------------------------------------');
  console.info('Press -h to go Home.');
  console.info('Press -e to Exit.');
  console.info('----------------------------------------------');

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  let clientUsername;

  // Event listener for the 'username' event
  client.on('username', (username) => {
    clientUsername = username;
  });

  rl.on('line', async (input) => {
    const message = input.trim();
    if (message === '-e') {
      rl.close(); // Close the readline interface
      exitApp();
    } else if (message === '-h') {
      // Check if the clientUsername is defined
      if (!clientUsername) {
        console.log('Waiting for username...');
        return;
      }

      const token = await getToken(clientUsername);
      client.disconnect();

      // create a new client connection
      const newClient = io('https://terminal-chat-app-production.up.railway.app', {
        auth: {
          token
        }
      });

      // Attach events to newClient
      attachEvents(newClient);

      // Display Home menu after successful authentication
      const homeOption = await getMenuOption();

      // Render menu interface according to what the user selects
      const chatRoom = await render[homeOption](newClient);

      // Start chat room messaging
      chatMessageInterface(newClient, chatRoom);
    }
    client.emit('chat message', chatRoom, message);
  });
}

module.exports = chatMessageInterface;

```

`chatMessageInterface.js` exports a function that takes two parameters: `client`, a `socket.io-client` instance, and `chatRoom`, the room where the chat is taking place.

It starts by logging some navigation information onto the user's terminal. It then creates a readline interface that allows for user inputs to be read.

The `client` listens for the `username` event and stores it in a `clientUsername` variable for later use.

The readline interface then listens for the `line` event which is triggered when a user enters a command-line input. The input is stored in a `message` variable, and this determines the next operation.

Next, the `exitApp` function is called when the `message` is `-e`.

If the message is `-h`, the client token is retrieved from the server before the client is disconnected from the `socket.io` server.

A new client connection is then created with the retrieved token to make sure the user is authenticated to create and join chat rooms. The other functions in the `else if` block will be explained later in the heading " **The**  **Node Script**".

Also, if the `message` is neither `-e` nor `-h`, the `chat message` event is triggered and is used to send messages to other users in the chat room.

<a name="the-socket-client-and-node-script"></a>
## The Socket Client and Node Script

At the root of the client folder, there contains `attachEvents.js`, which attaches events to the `socket.io-client` instance, and `commander.js`, the client's interface node script.

<a name="socket-client-events"></a>
#### Socket Client Events

```
// attachEvents.js
module.exports = (client) => {

    client.on('connect', () => { });

    // Handles 'chat message' event when another user sends a message
    client.on('chat message', (message) => {
      console.info(message);
    });

    client.on('joined', (info) => {
      console.info(info);
    });

    client.on('user joined', (info) => {
      console.info(info);
    });

    // // Handles 'user left' event when a user leaves a room
    client.on('user left', (info) => {
      console.info(info);
    });
}

```

`attachEvents.js` exports a function that takes the `socket.io-client` instance as a parameter. It listens to various events and specifies what to do when they're triggered.

<a name="the-node-script"></a>
#### The Node Script

```
#!/usr/bin/env node // commander.js
const { Command } = require('commander');
const io = require('socket.io-client');

const getAuthOption = require('./src/views/getAuthOption');
const getMenuOption = require('./src/views/getMenuOption');
const chatMessageInterface = require('./src/views/chatMessageInterface');
const render = require('./src/views/renderInterface');
const attachEvents = require('./attachEvents');

const program = new Command();

program.version('1.0.0').description('Terminal Chat App');

// Start Terminal chat app
program
  .description('Starts the Terminal chat app')
  .command('start').action(async () => {
    // Display Authentication menu
    const authOption = await getAuthOption();

    // Render authentication interface according to what the user selects
    const token = await render[authOption]();

    if (!token) {
      console.info('Authentication Error!');
      process.exit(1);
    }

    // connect to the socket server after authentication
    const client = io('https://terminal-chat-app-production.up.railway.app', {
      auth: {
        token
      }
    });

    // Attach events to client
    attachEvents(client);

    // Display Home menu after succesful authentication
    const homeOption = await getMenuOption();

    // Render menu interface according to what the user selects
    const chatRoom = await render[homeOption](client);

    // Start chat room messaging
    chatMessageInterface(client, chatRoom);
  }
  );

program.parse(process.argv);

```

The above file is a node script that uses the `commander` library for handling command-line arguments. The script only allows for the `start` command-line argument. This argument starts the app.

On starting the app, the authentication menu is displayed. The `render` object is then used to render what the user selects from the menu, and the result of the operation is stored in a `token` variable.

If a token was not returned from the previous operation, which translates to an authentication failure, the app is terminated. Otherwise, the app keeps running.

A `socket.io-client` instance is then created, and it uses the returned token for authentication when connecting to the `socket.io` server.

The home menu is displayed after a successful connection with the server. Again, the `render` object is then used to render what the user chooses from the home menu option. A `chatRoom` is returned from the operation.

The `socket.io-client` instance, along with the `chatRoom`, is then used to start the chat message interface.

The user can now start chatting with other users in the chat room in real-time.

The client code was published on the [NPM registry](https://www.npmjs.com/package/tarminal-chat-app). This is for easy access and installation on the command line anywhere in the world. You can run `npm install tarminal-chat-app` on your terminal to install the app.

In the next section, I review the challenges, lessons learned, and potential future improvements for the project.