Build a Real-Time Command-Line Chat App with Node.js and Chatkit

Real-time chat is an essential feature for many modern applications, but it can be tricky to implement. Managing WebSocket connections, authenticating users, and handling chat history at scale takes a lot of time and infrastructure. Luckily, services like Chatkit make it easy to add powerful chat features to your app without rolling your own backend.

In this in-depth tutorial, we‘ll walk through building acomplete command-line chat interface in Node.js powered by Chatkit. You‘ll learn how to:

  • Create and configure a Chatkit instance
  • Authenticate users and create user accounts
  • List, select, and subscribe to chat rooms
  • Send and receive messages in real-time
  • Implement helpful features like a loading spinner

By the end, you‘ll have a working chat app and a solid foundation for adding even more functionality. Let‘s get started!

Architecture Overview

Our chat app will have 3 main components:

  1. A Chatkit instance – This is a dedicated chat server and infrastructure hosted for us by Pusher. It will handle all the heavy lifting of managing connections, rooms, and messages.

  2. A Node.js authentication server – We‘ll write a simple Express server that creates Chatkit users and generates auth tokens. In a production app, you‘d want to add real auth and persist user IDs.

  3. A command-line client – This Node.js script will interact with the user via the terminal. It will prompt for a username, list rooms, send typed messages, and print incoming messages.

Here‘s a diagram of how the pieces fit together:

[Architecture Diagram]

The client will first make a request to the authentication server to create a Chatkit user and get an auth token. It then connects directly to our Chatkit instance using the token.

Step 1: Set Up a Chatkit Instance

Before we write any code, we need to create a Chatkit instance to power our chat features. Head over to the Chatkit Dashboard and hit "Create a new instance":

[Chatkit Dashboard Screenshot]

Give your instance a name and choose a region. I called mine "Command Line Chat" and picked the US region. Once it spins up, click into the "Console" tab and make note of two values: your Instance Locator and Secret Key. We‘ll use these to connect to Chatkit from our code.

Step 2: Create a Chatkit Room

Within your instance, click the "Create a new room" button. Let‘s make our first room "General Chat":

[Create Room Screenshot]

Rooms are where the magic happens in Chatkit. Messages are sent and received within a room. You can think of them like channels in Slack.

Later we‘ll see how to create rooms programmatically. You can also make private rooms and direct messages, but we‘ll stick to public rooms for now.

Step 3: Set Up the Authentication Server

Time to write some code! Create a new directory for the project and initialize a new Node app:

mkdir command-line-chat
cd command-line-chat
npm init -y

Then install the dependencies we need:

npm install --save express body-parser cors @pusher/chatkit-server

Create a file called server.js with the following:

const express = require(‘express‘);
const bodyParser = require(‘body-parser‘);
const cors = require(‘cors‘);
const Chatkit = require(‘@pusher/chatkit-server‘);

const app = express();

const chatkit = new Chatkit.default({
  instanceLocator: ‘YOUR_INSTANCE_LOCATOR‘,
  key: ‘YOUR_SECRET_KEY‘,
});

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());

app.post(‘/users‘, (req, res) => {
  const { username } = req.body;
  chatkit
    .createUser({
      id: username,
      name: username,
    })
    .then(() => {
      res.sendStatus(201);
    })
    .catch(err => {
      if (err.error === ‘services/chatkit/user_already_exists‘) {
        res.sendStatus(200);
      } else {
        res.status(err.status).json(err);
      }
    });
});

app.post(‘/authenticate‘, (req, res) => {
  const authData = chatkit.authenticate({
    userId: req.query.user_id,
  });
  res.status(authData.status).send(authData.body);
});

app.set(‘port‘, process.env.PORT || 3000);
const server = app.listen(app.get(‘port‘), () => {
  console.log(`Express running → PORT ${server.address().port}`);
});

Be sure to replace YOUR_INSTANCE_LOCATOR and YOUR_SECRET_KEY with your actual values.

This code sets up an Express server with two key endpoints:

  1. POST /users – Creates a new Chatkit user with the provided username. If the user already exists, it just returns 200 OK.

  2. POST /authenticate – Generates an auth token for the provided user ID. The client will send this token in the headers when connecting to Chatkit.

Let‘s run the server:

node server.js

You should see Express running → PORT 3000, indicating the server is listening for requests.

Step 4: Build the Command-Line Client

Now for the fun part – building the interactive command-line interface! Create a new file called client.js.

First, we need to install a few packages:

npm install --save @pusher/chatkit-client pusher-chatkit-node readline promptly ora

Here‘s what each one does:

  • @pusher/chatkit-client – The Chatkit client SDK for connecting to our instance and interacting with rooms/users.
  • pusher-chatkit-node – A Node helper for Chatkit required to use the client SDK outside of a browser.
  • readline – For reading individual lines of input from the console.
  • promptly – To prompt the user for input and handle validation.
  • ora – For displaying a fancy spinner while loading.

Now paste the following into client.js:

const Chatkit = require(‘@pusher/chatkit-client‘);
const readline = require(‘readline‘);
const promptly = require(‘promptly‘);
const ora = require(‘ora‘);
const ChatkitNode = require(‘pusher-chatkit-node‘);

// Helper to make Chatkit client work with Node
ChatkitNode.adapters[‘node-js‘] = require(‘pusher-chatkit-node/node-js‘);

const CHATKIT_INSTANCE_LOCATOR = ‘YOUR_INSTANCE_LOCATOR‘;
let currentUser;
let currentRoom;

async function connectToChatkit(username) {  
  const spinner = ora(‘Connecting to Chatkit‘).start();

  const chatManager = new Chatkit.ChatManager({
    instanceLocator: CHATKIT_INSTANCE_LOCATOR,
    userId: username,
    tokenProvider: new Chatkit.TokenProvider({ url: ‘http://localhost:3000/authenticate‘ }),
  });

  currentUser = await chatManager.connect();

  spinner.succeed(‘Connected to Chatkit‘);

  return currentUser;
}

async function listRooms() {
  const spinner = ora(‘Getting rooms‘).start();

  const joinableRooms = await currentUser.getJoinableRooms();
  const joinedRooms = currentUser.rooms;

  spinner.succeed(‘Rooms:\n‘);

  [...joinableRooms, ...joinedRooms].forEach((room, index) => {
    console.log(`${index}: ${room.name}`);
  });
}

async function promptForRoom() {
  let roomIndex = null;
  while (roomIndex === null) {
    roomIndex = await promptly.prompt(`Enter room number: `);
    if (roomIndex >= [...currentUser.rooms, ...currentUser.getJoinableRooms()].length) {
      console.log(‘Invalid room number‘);
      roomIndex = null;
    }
  }

  const rooms = [...currentUser.getJoinableRooms(), ...currentUser.rooms];
  const selectedRoom = rooms[roomIndex];
  currentRoom = selectedRoom;
  return currentRoom;
}

async function subscribeToRoom(room) {
  console.log(`Joining room: ${room.name}`);

  await currentUser.subscribeToRoom({
    roomId: room.id,
    hooks: {
      onMessage: message => {
        if (message.senderId !== currentUser.id) {
          const date = new Date(message.createdAt);
          console.log(`${message.senderId} @ ${date}: ${message.text}`);
        }
      },
    }
  });

  console.log(‘Joined successfully‘);
}

async function promptAndSendMessage() {
  const text = await promptly.prompt(‘:‘);
  await currentUser.sendMessage({
    text,
    roomId: currentRoom.id,
  });
}

async function createAccount() {
  const username = await promptly.prompt(‘Enter username: ‘);
  const spinner = ora(‘Creating user‘).start();

  const res = await fetch(‘http://localhost:3000/users‘, {
    method: ‘POST‘,
    headers: {
      ‘Content-Type‘: ‘application/json‘,
    },
    body: JSON.stringify({ username }),
  });

  if (!res.ok) {
    spinner.fail();
    throw new Error(`Failed to create user ${username}`);
  }

  spinner.succeed(`Created user ${username}`);

  return username;
}

async function main() {
  const username = await createAccount();
  const user = await connectToChatkit(username);
  await listRooms();
  const room = await promptForRoom();
  await subscribeToRoom(room);

  console.log(‘Listening for messages (press enter to send)‘);

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

  rl.on(‘line‘, promptAndSendMessage);
}

main();

Let‘s break this down piece by piece.

At the top, we import the dependencies and configure the Chatkit SDK to work with Node. Be sure to replace YOUR_INSTANCE_LOCATOR with your actual value.

The connectToChatkit function initializes a ChatManager with our instance details and a token provider URL that will call our authentication server. It then connects the current user.

listRooms gets a list of joinable rooms (all rooms for now since we only have public ones) and rooms the user has already joined. It prints them out with a number index to make selecting one easy.

promptForRoom asks the user to enter a room number and extracts the corresponding room from the list.

subscribeToRoom joins the selected room and registers a hook to print out new messages as they arrive, ignoring ones from the current user.

promptAndSendMessage waits for the user to type a message and send it to the current room when they hit enter.

createAccount prompts the user for a username, POSTs it to the /users endpoint to create a Chatkit user, and returns the name.

Finally, the main function orchestrates the flow: creating an account, connecting to Chatkit, listing/selecting a room, subscribing to the room, and then prompting for messages in a loop.

Let‘s test it out! Run the server in one terminal window:

node server.js

Then run the client in another:

node client.js

The client will prompt you for a username, then list out the available rooms. Enter the number for "General Chat" (or any other room you created). You should then be able to type a message and see it printed out.

Open another terminal and run the client again to simulate a second user. Send messages back and forth and you should see them show up in real-time in both clients!

[Chat Demo]

Next Steps

Congrats, you now have a working real-time command-line chat app! There are tons of ways you could extend it:

  • Room management – Add the ability to create, join, and leave rooms on the fly. Chatkit makes this easy with methods like createRoom and leaveRoom.

  • Improved authentication – Our simple auth server creates users on the fly with no real credentials. For a real app, you‘d want to implement password auth, restrict token generation to valid users, and tie Chatkit IDs to your own user IDs.

  • Presence – Chatkit has a built-in subscribeToPresenceEvents method that notifies clients when users come online or go offline. You could print a message when people join or leave the room.

  • Typing indicators – Let users see when someone is actively typing a message. Chatkit‘s startedTyping and stoppedTyping methods fire events you can subscribe to.

  • Read cursors – Display how far each user has read in the conversation, just like iMessage and WhatsApp. Chatkit can track the position of a cursor as the user scrolls.

The Chatkit Docs have details on all of these features and more so you can mix and match to build incredibly powerful chat interfaces with minimal boilerplate.

I hope this in-depth tutorial has given you the foundation to build amazing chat apps in Node.js with Chatkit. Feel free to use this as a starting point and add your own flair. Happy coding!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *