Jeremy Kithome Software Developer #MUFC To infinity and beyond! Fortune favours the bold. From tomato farmer to API farmer.

Making a chat app with Dapr

10 min read 2884

Introduction

Distributed Application Runtime(Dapr) is an open-source project by Microsoft. It is an event-driven, portable runtime that aims to simplify building microservice applications for developers. Dapr is composed of several building blocks accessed by standard HTTP or gRPC APIs and can be called from various programming languages.

Dapr layout
Source: https://cloudblogs.microsoft.com/opensource/2019/10/16/announcing-dapr-open-source-project-build-microservice-applications/

The main building blocks of Dapr are:

  • Service invocation – Service-to-service invocation enables method calls, including retries, on remote services wherever they are running in the supported hosting environment
  • State management – Allows for the storage, retrieval, and deletion of key/value pairs. Different data stores such as Redis, Azure CosmosDB, and DynamoDB are supported by different state store components
  • Publish and subscribe messaging between services – this enables event-driven architectures which make scaling horizontally significantly easier and adds failure resilience
  • Event-driven source bindings – The scalability and resilience of event-driven architectures are further improved by enabling receiving and sending events to external resources such as databases, queues, webhooks, etc
  • Virtual actors – A pattern for stateless and stateful objects that make concurrency simple with method and state encapsulation
  • Distributed tracing between services – It compiles trace events, metrics, and performance statistics between Dapr instances. This enables you to easily diagnose and monitor calls across multiples services in production using the W3C Trace context standard
  • Secrets management – It provides an easy way to access secrets without the need to know the intricacies of the specific secret store in use. Supported secret stores include Kubernetes, Azure Key Vault, AWS Secret manager, etc.

In this article, we will be exploring how to make a chat app using some of the building blocks.

Prerequisites

Our chat app will be built using Node.js and React. This article assumes that you are familiar with both and have a working knowledge of both. Before we begin building the chat app, ensure that you have Node, Yarn, or npm, and Docker installed on your machine. You can follow instructions in the provided links to install them if you haven’t already. We will use create-react-app to create the UI for our chat app.

Set up Dapr

The first thing we will need to do is install the Dapr CLI on our local machines. We will use the specific install script for our OS:

Linux:

$ wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

Windows:

$ powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex"

MacOS:

$ curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

Once you have installed the CLI, you will need to set up Dapr in your machine with the following command:

$ dapr init

installing dapr in the command line

You can now run Dapr locally with dapr run.

Set up subscriber

Our chat app will be composed of two microservices. The first microservice will be a Node application that will subscribe to published messages (sent messages) from the client UI(second microservice).

Create the following folder structure for our server:

We made a custom demo for .
No really. Click here to check it out.

node-subscriber
├── README.md
├── .gitignore
├── routes.js
└── app.js

Alternatively, this can be done through the terminal the following way:

$ mkdir node-subscriber
$ cd node-subscriber
$ touch README.md app.js routes.js .gitignore

You can add a description of what your project is about to the README.md. You should also add the node_modules folder to the .gitignore file like so:

node_modules/

To generate the package.json file without prompts, run the following command:

$ npm init -y

The contents of the package.json file will look like this:

{
  "name": "node-subscriber",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Install dependencies

$ yarn add express body-parser ws

In app.js we will create a simple express application that exposes a few routes and handlers and initializes a WebSocket server instance to send messages connected to clients. Add the following code to your app.js file:

const express = require("express");
const bodyParser = require('body-parser');
const WebSocket = require("ws");
const { createServer } = require("http");

const app = express();
app.use(bodyParser.json());
app.use("/", require("./routes"));

const port = process.env.PORT || 9000;

//initialize a http server
const server = createServer(app);
server.listen(port, () => {
  console.log(`Node subscriber server running on port: ${port}`);
});
//initialize the WebSocket server instance
const wss = new WebSocket.Server({ server });
wss.on("connection", (ws) => {
  console.info(`Total connected clients: ${wss.clients.size}`);
  app.locals.clients = wss.clients;
  //send immediate a feedback to the incoming connection
  ws.send(
    JSON.stringify({
      type: "connect",
      message: "Welcome to Dapr Chat App",
    })
  );
});

We start by creating a simple HTTP server using express before adding a WebSocket server that uses the express server. The connection event handler of the WebSocket server handles incoming connection requests from clients. We then assign the clients property of the WebSocket server to app.locals. This enables us to broadcast messages to all connected clients from any of the routed endpoints. Finally, we send a message to the client indicating a successful connection.

We expose a route /message that subscribes to messages from the client. The route handler will be defined in routes.js . Let’s add some code to send a simple message to connected clients:

const router = require("express").Router();
const WebSocket = require("ws");
const broadcast = (clients, message) => {
  clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
};
router.post("/message", (req, res) => {
  broadcast(req.app.locals.clients, "Bark!");
  return res.sendStatus(200);
});
module.exports = router;

Run node-subscriber with Dapr

We are ready to test our node-subscriber server now. Make sure that you are inside the node-subscriber directory. Run the node-subscriber app with Dapr dapr run --app-id node-subscriber --app-port 9000 --port 3500 node app.js.

The options app-id and app-port will be any unique identifier of our choice and the port that our Node application is running on. The port option indicates what port Dapr will run on. We also pass the command to run our app node app.js.

You can use the wscat utility or the Smart Websocket Client Chrome extension to test the WebSocket functionality of our server. If you have wscat installed, open a new terminal tab and run:

$ wscat -c ws://localhost:9000

Connect client to websocket

Once we have connected to the WebSocket, we can use the Dapr CLI post messages for testing and see if the connected clients receive a message:

dapr invoke --app-id nodeapp --method message --payload '{"message": "This is a test" }'

this is a test

wscat local host saying "welcome to the chat app"

After invoking the message method, the connected client receives a message from the app. Since the route should relay the message published from any client to other connected clients, let us make a few adjustments to the method for this:

const router = require("express").Router();
const WebSocket = require("ws");
const broadcast = (clients, text) => {
  clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({
        type: "message",
        text
      }));
    }
  });
};
router.post("/message", (req, res) => {
  const message = req.body.message;
  if(message) {
    broadcast(req.app.locals.clients, message);
  }
  return res.sendStatus(200);
});
module.exports = router;

Invoking the method with the sample message we used before produces the following result:dapr chat message

The server can now receive messages from a client and send it to other connected clients.

Chat app client

Setup

Our frontend client will be composed of a server and app. The server will invoke the message method through Dapr. Let us create the folder structure for our client:

$ mkdir dapr-chat-app-client
$ cd dapr-chat-app-client
$ touch README.md server.js .gitignore

Add a short description of the client to the README.md file and add the node_modules folder to the .gitignore file.

Next, generate the package.json file using the following command:

The contents of the package.json file will look like this:

{
  "name": "dapr-chat-app-client",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Install dependencies

$ yarn add express body-parser request

Go into the dapr-chat-app-client directory and bootstrap the frontend app using any of the following commands.

npx:

$ npx create-react-app client

npm:

$ npm init react-app client

yarn:

$ yarn create react-app client

Frontend server

The frontend server will invoke the message method of the node subscriber when a message is sent from the frontend app. Add the following code to the server.js file in the dapr-chat-app-client directory:

const express = require('express');
const path = require('path');
const request = require('request');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const daprPort = process.env.DAPR_HTTP_PORT || 3500;
const daprUrl = `http://localhost:${daprPort}/v1.0`;
const port = 8080;

app.post('/publish', (req, res) => {
  console.log("Publishing: ", req.body);
  const publishUrl = `${daprUrl}/invoke/node-subscriber/method/message`;
  request( { uri: publishUrl, method: 'POST', json: req.body } );
  res.sendStatus(200);
});

// Serve static files
app.use(express.static(path.join(__dirname, 'client/build')));
// For all other requests, route to React client
app.get('*', function (_req, res) {
  res.sendFile(path.join(__dirname, 'client/build', 'index.html'));
});
app.listen(process.env.PORT || port, () => console.log(`Listening on port ${port}!`));

The server file creates an express server that exposes a publish endpoint which invokes the message method on the node subscriber using Dapr. It also serves the static files for the frontend app.

For the client to run, we will need to build and start the frontend app we bootstrapped inside the client folder under dapr-chat-app-client directory.

We will need to add a few scripts to the package.json file of the main folder for this:

"scripts": {
    "client": "cd client && yarn start",
    "start": "node server.js",
    "buildclient": "cd client && npm install && npm run build",
    "buildandstart": "npm run buildclient && npm install && npm run start"
},

Run the app using Dapr:

dapr run --app-id dapt-chat-app-client --app-port 8080 npm run buildandstart

This will run the app in development mode and you can view it in the browser using the link http://localhost:8080/.

Client-side application

We are ready to work on the frontend app. Navigate to the client folder:

cd client

Installing additional dependencies

We will user Semantic UI React for styling.

To install it, run the following command:

$ yarn add semantic-ui-react

To theme the Semantic UI components, we will need the semantic ui stylesheets. The quickest way to get started is by using a CDN. Just add this link to the <head> of your index.html file in the public folder:

<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />

Component setup

Our application will be housed in the app.js file which will be the main component:

# Navigate to source directory
$ cd src/

When the main component mounts, we will create a WebSocket connection and store it using Refs. The connection will listen to messages from our node-subscriber server. The received messages will then be added to the state and processed for display. The initial setup will look something like this:

import React, { Fragment, useState, useEffect, useRef } from "react";
import {
  Header
} from "semantic-ui-react";
import "./App.css";

const App = () => {
  const webSocket = useRef(null);
  const [socketMessages, setSocketMessages] = useState([]);

  useEffect(() => {
    webSocket.current = new WebSocket("ws://localhost:9000");
    webSocket.current.onmessage = message => {
      const data = JSON.parse(message.data);
      setSocketMessages(prev => [...prev, data]);
    };
    webSocket.current.onclose = () => {
      webSocket.current.close();
    };
    return () => webSocket.current.close();
  }, []);
  return (
    <div className="App">
      <Header as="h2" icon>
        <Icon name="users" />
        Dapr Chat App
      </Header>
      <Grid>
      </Grid>
    </div>
  );
};
export default App;

Dapr chat app

To handle messages we receive from the node-subscriber server, we will use a useEffect that will fire whenever the socketMessages changes. It will take the last message and process it:

useEffect(() => {
    let data = socketMessages.pop();
    if (data) {
      switch (data.type) {
        case "message":
          handleMessageReceived(data.text);
          break;
        default:
          break;
      }
    }
  }, [socketMessages]);

The message will be processed and if the type of the message is message it will call the handleMessageReceived handler. This handler will update the state messages variable with new messages that will then be displayed to the user:

const Chat = ({ connection, updateConnection, channel, updateChannel }) => {
  ...  
  const messagesRef = useRef([]);
  const [messages, setMessages] = useState({});
  ...
  const handleMessageReceived = (text) => {
    let messages = messagesRef.current;
    let newMessages = [...messages, text];
    messagesRef.current = newMessages;
    setMessages(newMessages);
  };
  ...
}

The handler retrieves the currently stored messages and adds the newly received message before updating the value.

Now that we have handled receiving messages from the node-subscriber server, we need to display the chat messages. Additionally, we will have an input for the user to send a message to other users on the chat. If there are no messages to display, we will have a banner showing that no messages have been received yet.

banner displaying that no messages have been received yet

Update the app component with the message box code like this:

...
import {
  Icon,
  Input,
  Grid,
  Segment,
  Card,
  Comment,
  Button,
} from "semantic-ui-react";

const App = () => {
  ...
  const [message, setMessage] = useState("");
  ...
  const handleSubmit = (e) => {
    fetch('/publish', {
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        method:"POST",
        body: JSON.stringify({ message }),
    });
    e.preventDefault();
    setMessage('');
  }
  ...
  return (
    <div className="App">
      <Header as="h2" icon>
        <Icon name="users" />
        Dapr Chat App
      </Header>
      <Grid centered>
        <Grid.Column width={9}>
          <Card fluid>
            <Card.Content>
              {messages.length ? (
                  <Fragment>
                    {messages.map((text,id) => (
                      <Comment key={`msg-${id}`}>
                        <Comment.Content>
                          <Comment.Text>{text}</Comment.Text>
                        </Comment.Content>
                      </Comment>
                    ))}
                  </Fragment>
              ) : (
                <Segment placeholder>
                  <Header icon>
                    <Icon name="discussions" />
                    No messages available yet
                  </Header>
                </Segment>
              )}
              <Input
                fluid
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                placeholder="Type message"
                action
              >
                <input />
                <Button color="teal" disabled={!message} onClick={handleSubmit}>
                  <Icon name="send" />
                  Send Message
                </Button>
              </Input>
            </Card.Content>
          </Card>
        </Grid.Column>
      </Grid>
    </div>
  );
}

As the user types a message, the submit button will become enabled and the state message value will be set. When the user clicks on the Send Message button, we will call the handleSubmit event handler which will make a call to the frontend server with the message the user has typed.

The complete code for the app component of the frontend app will now look like this:

import React, { useState, useEffect, useRef, Fragment } from "react";
import {
  Header,
  Icon,
  Input,
  Grid,
  Segment,
  Card,
  Comment,
  Button,
} from "semantic-ui-react";
import "./App.css";

const App = () => {
  const [socketMessages, setSocketMessages] = useState([]);
  const webSocket = useRef(null);
  const [message, setMessage] = useState("");
  const messagesRef = useRef([]);
  const [messages, setMessages] = useState({});

  useEffect(() => {
    webSocket.current = new WebSocket("ws://localhost:9000");
    webSocket.current.onmessage = message => {
      const data = JSON.parse(message.data);
      setSocketMessages(prev => [...prev, data]);
    };
    webSocket.current.onclose = () => {
      webSocket.current.close();
    };
    return () => webSocket.current.close();
  }, []);

  useEffect(() => {
    let data = socketMessages.pop();
    if (data) {
      switch (data.type) {
        case "message":
          handleMessageReceived(data.text);
          break;
        default:
          break;
      }
    }
  }, [socketMessages]);

  const handleMessageReceived = (text) => {
    let messages = messagesRef.current;
    let newMessages = [...messages, text];
    messagesRef.current = newMessages;
    setMessages(newMessages);
  };

  const handleSubmit = (e) => {
    fetch('/publish', {
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        method:"POST",
        body: JSON.stringify({ message }),
    });
    e.preventDefault();
    setMessage('');
  }

  return (
    <div className="App">
      <Header as="h2" icon>
        <Icon name="users" />
        Dapr Chat App
      </Header>
      <Grid centered>
        <Grid.Column width={9}>
          <Card fluid>
            <Card.Content>
              {messages.length ? (
                  <Fragment>
                    {messages.map((text,id) => (
                      <Comment key={`msg-${id}`}>
                        <Comment.Content>
                          <Comment.Text>{text}</Comment.Text>
                        </Comment.Content>
                      </Comment>
                    ))}
                  </Fragment>
              ) : (
                <Segment placeholder>
                  <Header icon>
                    <Icon name="discussions" />
                    No messages available yet
                  </Header>
                </Segment>
              )}
              <Input
                fluid
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                placeholder="Type message"
                action
              >
                <input />
                <Button color="teal" disabled={!message} onClick={handleSubmit}>
                  <Icon name="send" />
                  Send Message
                </Button>
              </Input>
            </Card.Content>
          </Card>
        </Grid.Column>
      </Grid>
    </div>
  );
};
export default App;

messages sent in the chat app

completed chat app highlighting messages sent between users

And just like that, we’ve built a simple chat application using Dapr. This version of the chat application runs locally and is great for familiarizing yourself with the Dapr CLI. If you want to deploy your chat application to production you can do so using Kubernetes clusters. You can find instructions on how to setup the environment in the Dapr documentation.

Conclusion and next steps

The example chat app that we built has very limited functionality. You can add features such as the ability for a user to select a user name and start a session. In addition, you can broadcast users that have joined to other connected clients. Messages in the chatbox can also be improved to show the sender’s information and the time the message was sent. From this example, we can see that it is quite easy to write resilient scalable microservices using Dapr. You can find more information and examples in the Dapr GitHub repo. If you would like to look at the complete code for this example you can find it on the node-subscriber and the chat client repos on GitHub.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Jeremy Kithome Software Developer #MUFC To infinity and beyond! Fortune favours the bold. From tomato farmer to API farmer.

Leave a Reply