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
And install Odi.
npm install odi
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
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"
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"]
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!"));
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'
}
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
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;
}
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);
}
}
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);
}
}
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.
Odi automatically initializes injected fields, so just disable this check in
tsconfig.json
"strictPropertyInitialization": false
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"
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>
)
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:
- Message representation
- Controls (Send button, message input, username input)
- 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>
)
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>
)
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>
)
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>
)
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()}/>;
}
}
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"
}
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');
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;
}
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);
});
And the last step, onclinck
handler for our button.
button.onclick = () => {
socket.emit('massage:send', {
text: messageInput.value,
username: usernameInput.value
});
messageInput.value = "";
}
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
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!
🚀 Getting Started
-
Install npm package
npm install odi --save
-
Install reflect-metadata
npm install reflect-metadata --save
-
Import reflect-metadata (for example in
index.ts
):
import "reflect-metadata";
-
Enabled the following settings in
tsconfig.json
"emitDecoratorMetadata": true, "experimentalDecorators": true
🌪 Overview
Controller
Controllers serve as a simple yet powerful routing mechanism in a minimalistic style.
@Controller('foo')
export class FooController extends IController {
@RoutePatch('{id}'
…Also, if you are looking for more information, you can check previous articles and docs:
If you have any ideas or questions, feel free to leave them! Thanks a lot! :)
Top comments (0)