avatarRoss Bulat

Summarize

Typescript Live Chat: Express and Socket.io Server Setup

How to do real-time chat with Typescript: Part 1 of 2

Typescript has become a great tool for live chat

If you decide to develop real-time communication for your application, Typescript has become a very attractive option to do so, with its type safe guarantees and the range of packages readily available to facilitate the solution.

This two-part series [Part 2 here] will document the entire process of setting up a bare-bones Typescript live chat solution using the latest cutting-edge tooling at our disposal within the open source ecosystem. The setup allows any number of clients to connect to the service and receive incoming messages in real time. The full solution discussed here is available on Github to browse and demo.

This article introduces the project and documents the development process of the backend Express server, whereas part two will document the front end React client. The final solution is a working live chat environment:

Screencast of our live chat solution with Typescript, available on Github

Why Develop Typescript Live Chat?

Not only does developing your own chat solutions give you total freedom with what features to integrate, privacy concerns are alleviated by knowing where your messages are being routed to and stored at; a stark contrast to adopting a third party service.

In addition, third parties will undoubtedly be routing your messages for data analysis, weighting neural networks for their AI efforts, and more. Leveraging your own solution opens you up to the same capabilities, boosting your raw data and keeping it private in the process.

An in-house chat solution is not only viable for small teams — or even individuals, it is also maintainable and expandable when developed with Typescript in conjunction with other great tools we’ll document here.

To achieve a real-time chat solution we will utilise the following technologies:

  • Typescript Node Express server: Installation and setup will be documented, as well as how to run the server in dev mode, reloading the server when changes are made to our code
  • Typescript React App: The chat interface will be built in a Typescript Create React App project
  • Socket.io: A lightweight tool to manage websocket connections between your backend (Express server) and frontend (React App), providing event based communication that we will utilise to emit messages to all the connected clients. Our Socket.io server and client code will be structured in classes for exemplary modularity.
  • RxJS: We will also touch on the asynchronous event handling solution of our time, RxJS.

The backbone of the service lies in the Express server, that will listen to incoming Socket.io events and emit chat messages to connected clients. The following sections will walk through the process of installation and setup of this server.

Express Server Setup

The purpose of the Express server is to manage websocket connections, and to route incoming messages to all connected clients.

Your project folder structure can resemble the following to keep our React app and backend Express server within one enclosing folder:

# project folder structure
ts-livechat/
   backend/
   client/ # we will generate or clone the client template later

Our express server will reside in the backend/ folder. We will be initiating a new package.json and constructing our server from scratch. The alternative to this of course is to use a program such as express-generator to generate that boilerplate for us; but this is unnecessary for this project.

Installing dependencies

Inside backend/, initiate package.json and install the following dependencies:

# generate package.json
yarn init
# install dependencies
yarn add typescript express @types/express socket.io @types/socket.io
yarn add ts-node-dev --dev

We have installed express and socket.io, along with their associated types from DefinitelyTyped.

The ts-node-dev package has also been installed. This is an optimised Typescript recompile tool that relies on node-dev as a dependency. Node restarts and re-compiles are triggered upon every file change, alleviating the need to manually compiling Typescript and restarting your node.

Note: ts-node-dev is a more efficient alternative to using packages such as concurrently and nodemon; although these are widely used and worth familiarising with if the reader has interest of exploring these further.

Configuring package.json

We will want to add some scripts inside package.json to be able to run our server with ts-node-dev. We will call this dev mode. Along with this command, we’ll also include a prod command for compiling and running a production build:

// package.json
{
   ...
   "scripts": { 
      "dev": "ts-node-dev --respawn --transpile ./src/server.ts",
      "prod": "tsc && node ./dist/server.js"
   },
   ...
}
  • dev will run our server, with re-spawning enabled via the --respawn flag. --transpile speeds up compile time, but sacrifices type checking and declaration file generation.
  • prod will run and compile our Typescript to a dist/ folder and then run the resulting server.js file. Our server will be run via server.ts, which is where this filename derives from.

Note: If you are developing on a Mac, node-ts-dev will output notifications in your Notifications sidebar upon every server restart. I opted to disable this, which can be done in System Preferences -> Notifications, and turning off terminal-notifier.

Note 2: Although editors are out the scope of this talk, it is worth noting that VS Code is currently the best suited editor for Typescript; installing the TSLint extension will catch type errors as you are writing code.

Although there is no server to compile yet, we can now run yarn run dev to start our development server with auto-reloading, and yarn run prod to compile and run a compiled Javascript build of the server.

Configuring tsconfig.json

Housing a tsconfig.json file within backend/ will configure the Typescript compiler, accessed via the tsc command. Run tsv -v in your Terminal to ensure it is installed. The completed configuration has been documented below:

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": false,
    "target": "es6",
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "*": [
        "node_modules/",
        "src/types/*"
      ]
    }
  },
  "include": [
    "src/**/*"
  ]
}

This is all standard compiler configuration, adhering to the commonjs module framework and ES6 target. Note that compiled Javascript is generated in the dist/ folder, with our Typescript originating in the src/ folder.

Note: To explore tsconfig.json further, refer to the Typescript Handbook page dedicated to the file.

Running tsc will now adhere to the above configuration. With the environment configured we can now develop the Express server and Socket.io code itself, that can later be compiled into vanilla Javascript for production.

Server Structure

Our server will be developed within a src/ folder with the following structure, adhering to a modular design pattern:

backend/
   src/
      ChatServer.ts
      constants.ts
      server.ts
      types.ts
   dist/
 ...

We are mainly interested in ChatServer.ts, hosting a ChatServer class that manages everything from initialising our express server to our Socket.io connections and events. We will explore this file in more detail next.

server.ts simply initialises a new ChatServer object and exports the resulting express application:

// src/server.ts
import { ChatServer } from './ChatServer';
let app = new ChatServer().app;
export { app };

Also included are types.ts and constants.ts files that house one interface and one enum respectively:

// src/constants.ts
export enum ChatEvent {
  CONNECT = 'connect',
  DISCONNECT = 'disconnect',
  MESSAGE = 'message'
}

Our ChatEvent enum contains all the event types that Socket.io will emit:

  • The built-in connect and disconnect events that (in a self explanatory nature) fire when clients connect and disconnect from the server
  • A message event for handling incoming chat messages
// src/types.ts
export interface ChatMessage { 
   author: string;
   message: string;
}

types.ts stores all type and interface objects that we explicitly define, which for this demonstration only includes one interface. message events will receive a ChatMessage object, consisting of an author and the message itself. An example of such an object could be the following:

{
   author: 'Ross',
   message: "Testing chat message"
}

Socket.io sends and emits JSON objects, giving the developer flexibility with the complexity of data sent with each event. Once received, this object will then be emitted back to each connected client to update their chat window. This all happens in ChatServer.

In a production ready solution, it is very likely the emitting ChatMessage type will be different from the incoming type. For example, a timestamp could be added and displayed on the client side, to let users know how long it has been since the message was received.

Note: This demo has been designed to be bare-bones, yet fully typed and fully functional so the project can be easily built upon. Add to these files as your events become more complex.

ChatServer Class Implementation

The Express application is configured in ChatServer in its entirety. Let’s break down what is happening in the class.

Class properties

The class properties house our Express app instance, socket.io instance, and other configuration such as the port number of the socket service, all fully typed utilising the DefinitelyTyped packages we installed previously:

// ChatServer class properties
public static readonly PORT: number = 8080;
private _app: express.Application;
private server: Server;
private io: SocketIO.Server;
private port: string | number;

Note: Where you do not give default values for class properties, Typescript expects the constructor to provide those values instead. If not, the property must either be defined as optional with ?, or also have a null type.

The public PORT property acts as the default port for our server to listen on, defined as 8080. Our remaining properties are private, housing our application and server instances, as well as our socket.io instance. The private port property is a union type, that accepts either a string or a number type.

The Express app itself is stored as _app, which is private. To access _app externally, a getter has also been defined further down the class, returning _app as an express.Application type:

get app (): express.Application {
   return this._app;
}

And this is how server.ts gets _app and exports the resulting instance:

// src/server.ts
...
let app = new ChatServer().app; // calling the getter method here
export { app };

Constructor

The class constructor gives values for each of our properties, and initialises our server:

constructor () {
    this._app = express();
    this.port = process.env.PORT || ChatServer.PORT;
    this._app.use(cors());
    this._app.options('*', cors());
    this.server = createServer(this._app);
    this.initSocket();
    this.listen();
  }

There are a few interesting points we can derive from the constructor:

  • The port property firstly checks if an environment variable has been defined, and falls back to the default PORT property if it has not
  • We have included cors middleware to ensure access to the service from any domain, although you may wish to restrict this or remove this completely depending on your security model
  • A http server is instantiated via the imported createServer function from Express’s http package.
  • The initSocket() method is called, initialising our socketIo instance:
private initSocket (): void {
   this.io = socketIo(this.server);
}
  • The listen() method is called, which opens up communication to our server and Socket.io events.

Socket.io event handling

Let’s look at listen() in more detail to examine how these events are handled:

private listen (): void {
   // server listening on our defined port
   this.server.listen(this.port, () => {
      console.log('Running server on port %s', this.port);
   });
   //socket events
   this.io.on(ChatEvent.CONNECT, (socket: any) => {
      console.log('Connected client on port %s.', this.port);
      socket.on(ChatEvent.MESSAGE, (m: ChatMessage) => {
         console.log('[server](message): %s', JSON.stringify(m));
         this.io.emit('message', m);
      });
      socket.on(ChatEvent.DISCONNECT, () => {
         console.log('Client disconnected');
      });
   });
}

This boilerplate allows us to define what happens when the server starts listening on the specified port, as well as handle receiving Socket.io events.

Take note of the ChatEvent.MESSAGE event:

socket.on(ChatEvent.MESSAGE, (m: ChatMessage) => {
   console.log('[server](message): %s', JSON.stringify(m));
   this.io.emit('message', m);
 });

This event expects an incoming object of type ChatMessage, that we typed previously, to just have message and author values. We stringify and log this object, before emitting it to all connected clients via a message event. This is the mechanism by which messages are delivered to all the connected clients of the app.

As mentioned above, the returning ChatMessage will most likely be different in your production build, with timestamps, additional validations and other metadata associated with the message.

From here, the client side will need to listen to this message event and react to it to update the chat UX. Part two of this series will explore how this is done in more detail with React, Socket.io Client, and RxJS.

Backend Summary

We have now documented the Express server setup. The completed ChatServer.ts file along with the other backend/ source files can be found on Github here.

To summarise this process, we have:

  • Initiated a new Express project and installed our required dependencies, as well as their types
  • Configured the Typescript compiler, tsc, and defined build scripts inside package.json for development and production
  • Examined the usage of ts-node-dev and how to re-build and re-compile your Typescript as changes are being made
  • Introduced a modular project setup, separating exported, types, constants, and the chat server itself into coherent objects
  • Documented the ChatServer class, that utilises private class properties to store our server instances, with the class constructor initialising the server and Socket.io event listeners. A getter method was also defined for retrieving the Express application from an instantiated ChatServer, giving access while protecting the underlying private _app object from being manipulated externally.

In the next article we will hook up this backend service to a client side React app:

JavaScript
Typescript
Software Engineering
Web Development
Programming
Recommended from ReadMedium