DEV Community

Nicolas Felix
Nicolas Felix

Posted on

Real-time communication with Socket.io using Typescript

Typescript, according to its own website, is a “strongly typed programming language that builds on Javascript”. It can be seen as a superset of solutions and resources that makes Javascript more reliable.

Socket.IO is a “library that enables real-time, bidirectional and event-based communication between browser and the server”. It makes it easier to construct websocket-based solutions where the server can send updates to the browser in real-time.

In this article we will create a simple application implementing Socket.io using Typescript where the browser is updated via a third party http request. In this case we will have an order listing which is updated every time a new order arrives.

Setting up the project's structure

Let’s start by creating our server folder

mkdir websocket-typescript && cd websocket-typescript
Enter fullscreen mode Exit fullscreen mode

Then initialize our project

npm init
Enter fullscreen mode Exit fullscreen mode

set dist/app.js as entry point

In order to keep this project working as the updates come by, we will install our dependencies with specific versions:

# install typescript globally
npm install typescript -g

# dev-dependencies
npm i --save-dev @types/express@4.17.13 @types/node@16.6.2 ts-node@10.2.1 tslint@5.12.1 typescript@4.2.4

npm i --save class-transformer@0.3.1 class-validator@0.12.2 dotenv@10.0.0 express@4.17.1 routing-controllers@0.9.0 socket.io@4.1.3

# Initialize Typescript: 
tsc --init
Enter fullscreen mode Exit fullscreen mode

Now open your favorite text-editor and go to the root of our project. You'll find a tsconfig.json file there. This file indicates that it is a Typescript project.

Copy and paste this content into the tsconfig.json file replacing the initial one:

{
  "compilerOptions": {
      "module": "commonjs",
      "esModuleInterop": true,
      "target": "ES2015",
      "moduleResolution": "node",
      "sourceMap": true,
      "outDir": "dist",
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true
  },
  "lib": [
      "es2015"
  ]
}
Enter fullscreen mode Exit fullscreen mode
  • "module": "commonjs" Is the usually used for Node projects;
  • "esModuleInterop": true Will make sure that our imports behave normally;
  • "target": "ES2015" Helps to support ES2015 code;
  • "moduleResolution": "node" Specifically implies that this is a Node project;
  • "sourceMap": true Enables the generations of .map files;
  • "outDir": "dist" This is where our output files will be generated;
  • "emitDecoratorMetadata": true Enables experimental support for emitting type metadata for decorators which works with the module;
  • "experimentalDecorators": true Enables experimental support for decorators;
  • "lib": ["es2015"] This includes a default set of type definitions;

Now create a folder named src and a server.ts in it. Our folder structure will be divided in two: http and websocket.
Folder structure for websocket

This will be the initial content of our server.ts file:

require('dotenv').config()
import 'reflect-metadata';

import {
   createExpressServer,
   RoutingControllersOptions
} from 'routing-controllers'

const port = process.env.APP_PORT || 3000;

const routingControllerOptions: RoutingControllersOptions = {
   routePrefix: 'v1',
   controllers: [`${__dirname}/modules/http/*.controller.*`],
   validation: true,
   classTransformer: true,
   cors: true,
   defaultErrorHandler: true
}

const app = createExpressServer(routingControllerOptions);

app.listen(port, () => {
   console.log(`This is working in port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Now in the console type

tsc && node dist/server.js
Enter fullscreen mode Exit fullscreen mode

You should see this:
Application running on port
Note that we haven't configured nodemoon in this project, so as we change the server, you'll need to rerun this command

Socket.io in Node

So far nothing new. You've probably created lots of Node projects similar to this. Now here's where the fun begins. In order to have access to our Socket Server Instance in different parts of our application we will implement the Singleton Design Pattern. Within the websocket folder create a file called websocket.ts. This will be its initial content:

import { Server } from 'socket.io';

const WEBSOCKET_CORS = {
   origin: "*",
   methods: ["GET", "POST"]
}

class Websocket extends Server {

   private static io: Websocket;

   constructor(httpServer) {
       super(httpServer, {
           cors: WEBSOCKET_CORS
       });
   }

   public static getInstance(httpServer?): Websocket {

       if (!Websocket.io) {
           Websocket.io = new Websocket(httpServer);
       }

       return Websocket.io;

   }
}

export default Websocket;
Enter fullscreen mode Exit fullscreen mode

First we are importing the Server object from socket.io. Our class will inherit from it. Let’s take a look at the getInstance Method. It receives an optional parameter called httpServer and returns a Websocket instance. It checks if the private static attribute io is initialized. If not, it calls its own constructor and always returns a running instance of The Websocket implementation.

Let’s get back to our server.ts file now. To use the socket implementation we need to import it first:

import Websocket from './modules/websocket/websocket';
Enter fullscreen mode Exit fullscreen mode

Now in order to correctly implement this we have to change the way that our http server is created. That is because the Server object, which our Websocket class inherits from, expects an instance of NodeJS’s default http. Therefore in the beginning of the server.ts file we must add:

import { createServer } from 'http';
Enter fullscreen mode Exit fullscreen mode

Just after the creation of the constant app we must add:

const httpServer = createServer(app);
const io = Websocket.getInstance(httpServer);
Enter fullscreen mode Exit fullscreen mode

Last but not least, change the app.listen part to

httpServer.listen(port, () => {
   console.log(`This is working in port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

In order to separate the responsibilities of Sockets and Server, we must create a default pattern that each Socket class must implement. So add a file called mySocketInterface.ts to the websocket folder and add this to it:

import { Socket } from "socket.io";

interface MySocketInterface {

   handleConnection(socket: Socket): void;
   middlewareImplementation?(soccket: Socket, next: any): void

}

export default MySocketInterface;
Enter fullscreen mode Exit fullscreen mode

This is important because every socket-based class that we create from now on will implement this interface which will guarantee that we have exactly the methods that we need.

Without further ado we can finally create our orders.socket.ts file within the websocket folder. This file will be responsible for handling every socket connection regarding Orders. You may create other files in the future for different parts of your application. This will be its initial content:

import { Socket } from "socket.io";
import MySocketInterface from "./mySocketInterface";

class OrdersSocket implements MySocketInterface {

   handleConnection(socket: Socket) {

        socket.emit('ping', 'Hi! I am a live socket connection');

    }

   middlewareImplementation(socket: Socket, next) {
       //Implement your middleware for orders here
       return next();
   }
}

export default OrdersSocket;
Enter fullscreen mode Exit fullscreen mode

Since the OrdersSocket class implements MySocketInterface interface it is obligated to contain the handleConnection method. The middlewareImplementation method is optional and you can leave it out if you want.

Let’s get back to the websocket.ts file. We’ll now create a new method to initialize and handle each socket implementation that we have. This is what it will look like:

public initializeHandlers(socketHandlers: Array<any>) {
       socketHandlers.forEach(element => {
           let namespace = Websocket.io.of(element.path, (socket: Socket) => {
               element.handler.handleConnection(socket);
           });

           if (element.handler.middlewareImplementation) {
               namespace.use(element.handler.middlewareImplementation);
           }
       });
   }
Enter fullscreen mode Exit fullscreen mode

don't forget to change the import statement to

import { Server, Socket } from 'socket.io';
Enter fullscreen mode Exit fullscreen mode

This function is supposed to receive an array which will contain elements with information about each socket path and handler.

Now let’s get back to our server.ts file and enhance it. Import the OrderSocket class and just after the creation of the constant io add the following:

io.initializeHandlers([
   { path: '/orders', handler: new OrdersSocket() }
]);
Enter fullscreen mode Exit fullscreen mode

Great! To test all of this I've created a really simple html file which if you open in your browser you should see a message on screen if everything is right. You can download it here

Socket.io in the browser

Let's get started on the table and Http part no. We'll create a simple page to display information about the orders. I'm using bootstrap to make it slightly easier in terms of style, but feel free to use any framework of your choice.

You can download the index.html file here. We will only focus on the javascript part. The first thing we have to do once our page is loaded is to check for socket connection and once it is established emit an event requesting the initial orders listing, so create a index.js file and paste this as initial content:

const socket = io("http://localhost:3000/orders");

socket.on('connect', () => {
    socket.emit('request_orders');
});

socket.on('orders_updated', (orders) => {
    populateTable(orders.data);
})

socket.on('disconnect', () => {
    console.error('Ops, something went wrong');
});

function populateTable(data) {
    data.forEach(order => {
        document.querySelector('#orders-table tbody')
            .insertAdjacentHTML('afterend', createTableRow(order));
    });
}

function createTableRow(order) {
    let tRow = `<tr>
            <th scope="row">${order.id}</th>
            <td>${order.date}</td>
            <td>${order.total}</td>
            <td>${order.status}</td>
        </tr>`;

    return tRow;

}
Enter fullscreen mode Exit fullscreen mode

We'll now get back to Node to create the endpoint in which we'll receive new orders. It is a good practice to set your business rules in a service file. And that's what we'll do. Create a libs folder and a orders.service.ts file in it:
lib folder

This will be the file content:

import Websocket from "../modules/websocket/websocket";

class OrdersService {

    public insertOrder(order) {
        //save in your database

        //send the update to the browser
        this.updateSockets(order);
    }

    private updateSockets(order) {
        const io = Websocket.getInstance();
        io.of('orders').emit('orders_updated', { data: [order] });
    }
}

export default OrdersService;
Enter fullscreen mode Exit fullscreen mode

This is quite simple, but we're getting an instance of the Websocket class and emitting an event which our frontend file will listen and then update our table.

Now create a file orders.controller.ts within the http folder. This will be its content:

import { JsonController, Post, Body } from "routing-controllers";
import OrdersService from "../../libs/orders.service";

@JsonController('/orders', { transformResponse: true })
class OrdersController {

   @Post('/')
   insertOrder(@Body() order: any) {
       let orderService = new OrdersService();
       orderService.insertOrder(order);

       return {
           status: 200,
           success: true
       };
   }
}

export default OrdersController;
Enter fullscreen mode Exit fullscreen mode

Here the routing-controllers lib is helping us set a path to the order route for our web server and we're simpl calling the orders.service file that we just created.

Ok so go ahead to postman and send a POST request to http://localhost:3000/v1/orders/ with this content:

{
   "id": "4",
   "date": "2021-11-05",
   "total": "$13.00",
   "status": "Pending"
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to rerun the command to compile typescript in Node and check the table. It should be updating as the requests are sent.

orders table

That's all folks

This is just a sketch and one of the many ways to build a Socket.io based application. Feel free to leave a comment on possible better solutions =]

References

https://www.typescriptlang.org/
https://socket.io/docs/v4/
https://socket.io/docs/v4/namespaces/
https://socket.io/docs/v4/middlewares/
https://www.typescriptlang.org/tsconfig
https://dev.to/rajat19/create-a-new-node-js-project-in-typescript-nao
https://developer.mozilla.org/pt-BR/docs/Web/API/Element/insertAdjacentHTML
https://github.com/typestack/routing-controllers

Top comments (4)

Collapse
 
ifndefx profile image
pradt

Thanks for posting this, I'm getting type errors in the code and I wanted check is

constructor(httpServer) {
super(httpServer, {
cors: WEBSOCKET_CORS
});
}

equivalent to

constructor(httpServer : http.Server) {
super(httpServer, {
cors: WEBSOCKET_CORS
});
}

i.e httpServer has a type of http.Server ?

Collapse
 
yelsino profile image
yelsino

This information is worth millions, thank you very much. I wish you could answer me what type of data is the httpServer parameter, my editor is complaining :(

Collapse
 
sanjayadhikari profile image
Sanjay Adhikari • Edited

My import lines look like this:

import {Server, Socket} from 'socket.io';
import {Server as HttpServer} from 'http';

you will be good to go

Collapse
 
ifndefx profile image
pradt

Also, you have the html and JS file for the frontend in the resources folder. How do you access that, there are no routes etc... to that location.