DEV Community

Cover image for Building chat with Odi (Node.js)
Dan T.
Dan T.

Posted on • Originally published at Medium

Building chat with Odi (Node.js)

Republish

Sorry for republishing the same story. My teammate accidentally deleted the previous publication. He thought that we published it before the release date 😢 Thanks for understanding 😜

Introduction

The chat application is the very common example that used to show simple real-time communication between client and server. This tutorial describes how it can be easily done with Odi,TypeScript server-side framework for
Node.js.

Project Setup

We are going to develop the application that not only establishes the real-time communication channel but also renders frontend to the client, including required assets.

Basic Settings

Let’s set up the Odi project. First of all, we should initialize package.json
and tsconfig.json files. We can do it with two simple commands.

    npm init -y
    tsc --init
Enter fullscreen mode Exit fullscreen mode

And install Odi.

    npm install odi
Enter fullscreen mode Exit fullscreen mode

Also, we need to modify tsconfig.json file, as there are few options that must be edited. Odi actively uses decorators and metadata, so we need to enable these features.

    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
Enter fullscreen mode Exit fullscreen mode

Another thing is target option. By default, it set toes5 but there are
several things that are not supported in is this specification. As we are
progressive, let’s set it to the latest version

    "target": "ES2018"
Enter fullscreen mode Exit fullscreen mode

Project Structure

We are going to have different folders for views, assets and server source code.

  • src— server source code.
  • views— JSX templates that will be rendered to clients.
  • assets— client-side JS and CSS files.

JSX templates are tsx files that must be compiled. Add views folder to
rootDirs in tsconfig.json file and setup outDir.

    "outDir": "./build",
    "rootDirs": ["./src", "./views"]
Enter fullscreen mode Exit fullscreen mode

Gettings started

Odi is based on the Dependency Injection pattern, so every application component will be automatically imported, instantiated and injected.

Only folder with source files must be specified, then Odi can scan it for importing application components (Controllers, Services, Repositories and etc).

Main File

Create index.ts file in src folder. It will be the server entry point file.

    import { Core } from "odi";
    import { join } from "path";

    new Core({
        sources: __dirname,
        server: {
            port: 8080,
            socket: true,
            static: {
                root: join(__dirname, '../../assets'),
                prefix: '/assets'
            }
        }
    }).listen(() => console.log("Server successfully started!"));
Enter fullscreen mode Exit fullscreen mode

We just need to instantiate Core class. Core constructor accepts a single argument, settings object. There are a lot of possible options, but for now, we need only several of them.

First of all, we need to specify sources property. It’s required setting for Odi application. As index.ts file in src folder, which we choose for server-side code, we can use __dirname to set current directory.

port property is also required. It binds the server on the specified port.

Now about the following part:

    socket: true,
    static: {
         root: join(__dirname, '../../assets'),
         prefix: '/assets'
    }
Enter fullscreen mode Exit fullscreen mode

We must enable sockets and set options for serving static files All files from the assets folder are available by URL with /assets prefix.

Installing Dependencies

Odi framework automatically includes only several packages that are required. All other dependencies for different features are optional, so they need to be installed only if you use a certain feature.

For example, if you are going to build a simple REST server, you don’t need GraphQL, WebSockets, SSR and other packages.

We want to have WebSockets and Templating (JSX) in our chat application. So, let’s install missing packages:

    npm install socket.io react react-dom
Enter fullscreen mode Exit fullscreen mode

That’s all, Odi will automatically import it. As you can see, socket.iois used under the hood for real-time functionality. Also React packages is required for templates processing.

Now we can start writing our code :)

Application

We are going to create a web server, that renders HTML to the client, using
templates, serves files for the client (JS, CSS) and set up a real-time
communication channel using WebSockets for chat. Let’s add history to our chat. So, the last 10 messages will be saved in our system.

Message and History

Message will be pretty simple, only username and text fields. We can do it
with a simple interface, as we are not going to use a database.

    export interface Message {
        username: string;
        text: string;
    }
Enter fullscreen mode Exit fullscreen mode

And history service

    @Service()
    export default class HistoryService {
        private store: Message[] = [];

        getMessages() {
            return this.store;
        }

        addMessage(message: Message) {
            if(this.store.length > 10)
                this.store.shift();

            this.store.push(message);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Our store is a simple array of messages. And few methods for store management. If we get more than 10 messages, we simply remove the first message from the array.

As you can see, Service decorator was used for HistoryService class to set is as a service component. Service is singleton in Dependency Injection Container. Now it can be injected into others application components.

Put all this code in history.ts file in src/services folder.

Web Socket

Create chat.socket.ts file in the src/sockets directory with the following
code.

    import { Socket, OnEvent, ISocket, Autowired } from "odi";
    import HistoryService, { Message } from "../services/history";

    @Socket('chat')
    export default class ChatSocket extends ISocket {

        @Autowired()
        history: HistoryService;

        @OnEvent('massage:send') 
        onmessage(message: Message) {
           this.history.addMessage(message); 
           this.emit('message:new', message);
        }
    }
Enter fullscreen mode Exit fullscreen mode

We defined /chat namespace with handler for message:send event. If
message:send event is fired, all clients that connected to this namespace will be notified with message:new event and message data.

As you can notice Socket decorator defines namespaces. Leading slash is not required. To set up method as the handler for certain event, use OnEvent
decorator, that accepts event name as the argument.

Also, we injected HistoryService using Autowired decorator. history field
of ChatSocket class will be initialized by Odi, so you don’t need to do
anything additional.

The only thing, you can see such error from TypeScript

    [ts] Property 'history' has no initializer and is not definitely assigned in the constructor.
Enter fullscreen mode Exit fullscreen mode

Odi automatically initializes injected fields, so just disable this check in
tsconfig.json

    "strictPropertyInitialization": false
Enter fullscreen mode Exit fullscreen mode

Templating (JSX)

There a lot of templating processors — EJS, Jade, Pug. But there are a lot of
limitations and inconveniences with those technologies. In most cases, to have IntelliSense and code highlight for templates, you need to install an extension for IDE/Editor.

In Odi, JSX powered by React is used for templating. You can simply create components with JSX. But remember, it’s only for templates, any logic, listeners or client-side code will be ignored during rendering.
(Currently, we are working on full SSR.Hope it will be released soon)

We need to tell TypeScript compiler, that we are going to use React JSX.
In tsconfig.json

    "jsx": "react"
Enter fullscreen mode Exit fullscreen mode

Layout

Let’s create our layout component layout.view.tsx that will be a wrapper for all pages. As was mentioned above, all templates will be in views folder.

    import React, { SFC } from 'react';

    export const Html: SFC = ({ children }) => (
        <html lang="en">
            <head>
                <meta charSet="UTF-8" />
                <meta name="viewport" />
                <meta httpEquiv="X-UA-Compatible" content="ie=edge"/>
                <link href="/assets/index.css" type="text/css" ... />
                <title> Simple chat </title>
            </head>
            <body>
                {children}
            </body>

            <script src="path/to/socket.io" />
            <script src="/assets/index.js" />
        </html>
    )
Enter fullscreen mode Exit fullscreen mode

For socket.io-client library we can use CDN. So simply replace
path/to/socket.io in the script tag with the following link
https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js

Client js file was specified in the second script tag. We will create it a
little bit later in assets folder.

Chat Components

Actually, we need 3 components for our chat:

  1. Message representation
  2. Controls (Send button, message input, username input)
  3. Chat container

I think we can put all these components in 1 file, chat.view.tsx

    import React from 'react';
    import { Message } from './services/history.service';

    export const ChatMessage = ({ username, text }: Message) => (
        <div>
            <b>{username}: </b>
            <span>{text}</span>
        </div>
    )
Enter fullscreen mode Exit fullscreen mode

We can use Message interface as props type for ChatMessage component.

Let’s add chat controls. id attribute was used for convenience, as we are
going to use js on the client side without any libs or frameworks.

    export const ChatControlls = () => (
        <div className="message-box">
            <input placeholder="User" id="user-input" />
            <input placeholder="Message" id="message-input" />
            <button> Send </button>
        </div>
    )
Enter fullscreen mode Exit fullscreen mode

And the last thing, chat wrapper.

    interface ChatProps {
        messages: Message[];
    }

    export const Chat = ({ messages }: ChatProps) => (
        <div className="chat">
            <div className="container">
              {messages.map(msg,i) => <ChatMessage key={i} {...msg} /> )}
             </div>
            <ChatControlls />
        </div>
    )
Enter fullscreen mode Exit fullscreen mode

This component accepts an array of messages (our history) in props to render it on page load.

Now we can put everything together and define our page component page.view.tsx

    import React from 'react';
    import { Chat } from './chat.view';
    import { Html } from './layout.view';
    import { Message } from './services/history.service';

    interface ChatPageProps {
        history: Message[];
    }

    export const ChatPage = ({ history }: ChatPageProps) => (
        <Html>
            <Chat messages={history} />        
        </Html>
    )
Enter fullscreen mode Exit fullscreen mode

That’s all about templating for our chat application. I have several lines of
CSS that I will include it in the source code, that you can find at the end of
the article.

We can move to controllers.

Controllers

Controllers serve as a simple yet powerful routing mechanism. Controller methods are mapped to web server paths. The value returned by the method is sent as the response.

In order to create a Controller, you must use the @Controller decorator and inherit the IController class. The decorator sets the component type, so the DI (dependency injection) container can detect what the class will be used for.

For our chat, we need only one controller to render a template to the client. As we are going to use JSX inside the controller file, it must have tsx file
extension.

So, let’s create render.controller.tsx in src/controllers folder.

    import React from 'react';
    import { Controller, IController, Get, Autowired } from "odi";
    import { ChatPage } from '../../views/page.view';
    import HistoryService from '../services/history.service';

    @Controller()
    export default class RenderController extends IController {

        @Autowired()
        history: HistoryService;

        @Get index() {
            return <ChatPage history={this.history.getMessages()}/>;
        }
    }
Enter fullscreen mode Exit fullscreen mode

As you can see, we injected our HistoryService into history property. Also, the handler for / path with Get method was defined. We can simply return our JSX component as a result, Odi automatically detects that it’s a template and renders it as simple HTML for the client (web browser).

Starting Application

Now, we can start our application and see what we got. Let’s specify start script in package.json file:

    "scripts": {
        "start": "tsc && node build/src/index.js"
    }
Enter fullscreen mode Exit fullscreen mode

Running npm start command compile our source code and run server entry file.

Let’s open the browser and check localhost:8080

As you can see, we have just empty chat without any functionality, as we did not specify the client index.js into assets folder.

Client

First of all, let’s get references for chat container and controls.

    const button = document.querySelector('button');

    const messageInput = document.querySelector('#message-input');
    const usernameInput = document.querySelector('#user-input');
    const container = document.querySelector('.container');
Enter fullscreen mode Exit fullscreen mode

When a new message comes, we need to append it as a child in container
element. We need the function for creating elements that represent messages.

    function createMessage({ username, text }) {
        const element = document.createElement('div');

        element.innerHTML = `
            <b>${username}: </b>
            <span>${text}</span>
        `;

        return element;
    }
Enter fullscreen mode Exit fullscreen mode

Then, let’s connect to our chat namespace and add the event handler for message:new event. When this event is fired, the message element will be appended to the container.

    const socket = io('/chat');
    socket.on('message:new', message => {
        const messageElement = createMessage(message);
        container.appendChild(messageElement);
    });
Enter fullscreen mode Exit fullscreen mode

And the last step, onclinck handler for our button.

    button.onclick = () => {
        socket.emit('massage:send', { 
            text: messageInput.value, 
            username: usernameInput.value
        });

        messageInput.value = "";
    }
Enter fullscreen mode Exit fullscreen mode

We are collecting data from inputs and sending it as message:send event. Also, the message input text will be cleared after every send.

Now we can refresh the page, and see what we have got.

After refreshing the page, we will have history our messaging.

Sandbox

You can check the source code and interact with the application right here:

PS

Thanks for the reading! If you like Odi, please support us with a simple start on GitHub

GitHub logo Odi-ts / odi

🌪🌌 Opinionated, Declarative, Idiomatic framework for building scalable, supportable and reliable enterprise applications.

TypeScript framework for creating enterprise-grade (web) applications with simple and minimalistic API, that allows you to focus on business logic. Based on declarative and imperative programming, inspiried by ASP.NET / Spring.

Check Docs for more details.

Odi provides feature set for creation of easy supportable and scalable web applications.

Features Overview:

  • MVC
  • Full-typed DI / IoT
  • Authentication
  • WebSockets
  • TypeORM integration
  • GraphQL
  • AOP
  • SSR

For future updates check Roadmap
Got an idea, proposal or feature request? Feel free to Submit it!

Edit Odi

🚀 Getting Started

  1. Install npm package
    npm install odi --save

  2. Install reflect-metadata
    npm install reflect-metadata --save

  3. Import reflect-metadata (for example in index.ts):
    import "reflect-metadata";

  4. Enabled the following settings in tsconfig.json

    "emitDecoratorMetadata":  true, 
    "experimentalDecorators":  true
    Enter fullscreen mode Exit fullscreen mode

🌪 Overview

Controller

Controllers serve as a simple yet powerful routing mechanism in a minimalistic style.

@Controller('foo')
export class FooController extends IController {      
        
    @RoutePatch('{id}'
…
Enter fullscreen mode Exit fullscreen mode

Also, if you are looking for more information, you can check previous articles and docs:

  1. Docs
  2. First Article
  3. Second Article

If you have any ideas or questions, feel free to leave them! Thanks a lot! :)

Top comments (0)