DEV Community

Cover image for Build a real-time chat application with Nestjs and PostgreSQL
Arctype Team for Arctype

Posted on • Originally published at arctype.com

Build a real-time chat application with Nestjs and PostgreSQL

The code for this tutorial is available on my Github repository. Feel free to clone it as you follow the steps. Let's begin!

What is NestJS?

NestJS is a Node.js framework for creating fast, testable, scalable, loosely coupled server-side applications that use TypeScript. It takes advantage of powerful HTTP server frameworks such as Express or Fastify. Nest adds a layer of abstraction to Node.js frameworks and exposes their APIs to developers. It supports database management systems like PostgreSQL and MySQL. NestJS also offers dependency injections Websockets and APIGetaways.

What is a Websocket?

A WebSocket is a computer communications protocol that provides full-duplex communication channels over a single TCP connection. The IETF standardized the WebSocket protocol as RFC 6455 in 2011. The current specification is known as the HTML Living Standard. Unlike HTTP/HTTPS, Websocket are stateful protocols, which means the connection established between the server and the client will be alive unless terminated by the server or client; once a WebSocket connection is closed by one end, it extends to the other end.

Prerequisites

This tutorial is a hands-on demonstration. To follow along, ensure you have installed the following:

Project setup

Before diving into coding, let's set up our NestJS project and our project structure. We'll start by creating the project folder. Then, open your terminal and run the following command:

mkdir chatapp && cd chatapp
Enter fullscreen mode Exit fullscreen mode

Creating the project folder

Then install the NestJS CLI with the command below:

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

When the installation is complete, run the command below to scaffold a NestJS project.

nest new chat
Enter fullscreen mode Exit fullscreen mode

Choose your preferred npm package manager. For this tutorial, we'll use npm and wait for the necessary packages to be installed. Once the installation is completed, install WebSocket and Socket.io with the command below:

npm i --save @nestjs/websockets @nestjs/platform-socket.io
Enter fullscreen mode Exit fullscreen mode

Then, create a gateway application with the command below:

nest g gateway app
Enter fullscreen mode Exit fullscreen mode

Now let's start our server by running the command below:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Setting up a Postgres database

We can now set up our Postgres database to store our user records with our server setup. First, we'll use TypeORM (Object Relational Mapper) to connect our database with our application. To begin, we'll need to create a database with the following steps. First, switch to the system's Postgres user account.

sudo su - postgres
Enter fullscreen mode Exit fullscreen mode

Then, create a new user account with the command below.

createuser --interactive
Enter fullscreen mode Exit fullscreen mode

Next, create a new database. You can do that with the following command:

createdb chat
Enter fullscreen mode Exit fullscreen mode

Now, we'll connect to the database we just created. First, open the app.module.ts file, and add the following code snippet below in the array of imports[]:

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { Chat } from './chat.entity';
imports: [
   TypeOrmModule.forRoot({
     type: 'postgres',
     host: 'localhost',
     username: '<USERNAME>',
     password: '<PASSWORD>',
     database: 'chat',
     entities: [Chat],
     synchronize: true,
   }),
   TypeOrmModule.forFeature([Chat]),
 ],
...
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, we connected our application to a PostgresSQL database using the TypeOrmModule forRoot method and passed in our database credentials. Replace and with the user and password you created for the chat database.

Creating our chat Entity

Now that we've connected the application to your database create a chat entity to save the user's messages. To do that, create a chat.entity.ts file in the src folder and add the code snippet below:

import {
 Entity,
 Column,
 PrimaryGeneratedColumn,
 CreateDateColumn,
} from 'typeorm';

@Entity()
export class Chat {
 @PrimaryGeneratedColumn('uuid')
 id: number;

 @Column()
 email: string;

 @Column({ unique: true })
 text: string;

 @CreateDateColumn()
 createdAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, we created the columns for our chats using the Entity, Column, CreatedDateColumn, and PrimaryGenerateColumn decorators provided by TypeOrm.

Setting up a web socket

Let's set up a Web socket connection in our server to send real-time messages. First, we'll import the required module we need with a code snippet below.

import {
 SubscribeMessage,
 WebSocketGateway,
 OnGatewayInit,
 WebSocketServer,
 OnGatewayConnection,
 OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { AppService } from './app.service';
import { Chat } from './chat.entity';
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, we imported SubscribeMessage() to listen to events from the client, WebSocketGateway(), which will give access to socket.io; we also imported the OnGatewayInit, OnGatewayConnection, and OnGatewayDisconnect instances. This WebSocket instance enables you to know the state of your application. For example, we can have our server do stuff when a server joins or disconnects from the chat. Then we imported the Chat entity and the AppService which exposes the methods we need to save our user's messages.

@WebSocketGateway({
 cors: {
   origin: '*',
 },
})
Enter fullscreen mode Exit fullscreen mode

To enable our client to communicate with the server, we enable CORS by in initializing the WebSocketGateway.

export class AppGateway
 implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
 constructor(private appService: AppService) {}

 @WebSocketServer() server: Server;

 @SubscribeMessage('sendMessage')
 async handleSendMessage(client: Socket, payload: Chat): Promise<void> {
   await this.appService.createMessage(payload);
   this.server.emit('recMessage', payload);
 }

 afterInit(server: Server) {
   console.log(server);
   //Do stuffs
 }

 handleDisconnect(client: Socket) {
   console.log(`Disconnected: ${client.id}`);
   //Do stuffs
 }

 handleConnection(client: Socket, ...args: any[]) {
   console.log(`Connected ${client.id}`);
   //Do stuffs
 }
}
Enter fullscreen mode Exit fullscreen mode

Next, in our AppGateWay class, we implemented the WebSocket instances we imported above. We created a constructor method and bind our AppService to have access to its methods. We created a server instance from the WebSocketServer decorators.

Then we create a handleSendMessage using the @SubscribeMessage() instance and a handleMessage() method to send data to our client-side.

When a message is sent to this function from the client, we save it in our database and emit the message back to all the connected users on our client side. We also have many other methods you can experiment with, like afterInit, which gets triggered after a client has connected, handleDisconnect, which gets triggered when a user disconnects. The handleConnection method starts when a user joins the connection.

Creating a controller/service

Now let’s create our service and controller to save the chat and render our static page. Open the the app.service.ts file and update the content with the code snippet below:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Chat } from './chat.entity';

@Injectable()
export class AppService {
 constructor(
   @InjectRepository(Chat) private chatRepository: Repository<Chat>,
 ) {}
 async createMessage(chat: Chat): Promise<Chat> {
   return await this.chatRepository.save(chat);
 }

 async getMessages(): Promise<Chat[]> {
   return await this.chatRepository.find();
 }
}
Enter fullscreen mode Exit fullscreen mode

Then update the app.controller.ts file with the code snippet below:

import { Controller, Render, Get, Res } from '@nestjs/common';
import { AppService } from './app.service';
import { Chat } from './chat.entity';

@Controller()
export class AppController {
 constructor(private readonly appService: AppService) {}

 @Get('/chat')
 @Render('index')
 Home() {
   return;
 }

 @Get('/api/chat')
 async Chat(@Res() res) {
   const messages = await this.appService.getMessages();
   res.json(messages);
 }
}
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, we created two routes to render our static page and the user’s messages.

Serving our static page

Now let’s configure the application to render the static file and our pages. To do that, we'll implement server-side rendering. First, in your main.ts file, configure the application to static server files with the command below:

async function bootstrap() {
 ...
 app.useStaticAssets(join(__dirname, '..', 'static'));
 app.setBaseViewsDir(join(__dirname, '..', 'views'));
 app.setViewEngine('ejs');
 ...
}
Enter fullscreen mode Exit fullscreen mode

Next, create a static and a views folder in your src directory. In the views folder, create an index.ejs file and add the code snippet below:

<!DOCTYPE html>
<html lang="en">

<head>
 <!-- Required meta tags -->
 <meta charset="utf-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />

 <!-- Bootstrap CSS -->
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
   integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />

 <title>Let Chat</title>
</head>

<body>
 <nav class="navbar navbar-light bg-light">
   <div class="container-fluid">
     <a class="navbar-brand">Lets Chat</a>
   </div>
 </nav>
 <div class="container">
   <div class="mb-3 mt-3">
     <ul style="list-style: none" id="data-container"></ul>
   </div>
   <div class="mb-3 mt-4">
     <input class="form-control" id="email" rows="3" placeholder="Your Email" />
   </div>
   <div class="mb-3 mt-4">
     <input class="form-control" id="exampleFormControlTextarea1" rows="3" placeholder="Say something..." />
   </div>
 </div>
 <script src="https://cdn.socket.io/4.3.2/socket.io.min.js"
   integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQNs"
   crossorigin="anonymous"></script>
 <script src="app.js"></script>
 <!-- Option 1: Bootstrap Bundle with Popper -->
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
   integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
   crossorigin="anonymous"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

To speed things up in our templates, we used Bootstrap to add some stylings. Then we added two input fields and an unordered list to display the user's messages. We also included our app.js file which we will be creating later in this section and a link to the socket.io client.

Now create an app.js file and add the code snippet below:

const socket = io('http://localhost:3002');
const msgBox = document.getElementById('exampleFormControlTextarea1');
const msgCont = document.getElementById('data-container');
const email = document.getElementById('email');

//get old messages from the server
const messages = [];
function getMessages() {
 fetch('http://localhost:3002/api/chat')
   .then((response) => response.json())
   .then((data) => {
     loadDate(data);
     data.forEach((el) => {
       messages.push(el);
     });
   })
   .catch((err) => console.error(err));
}
getMessages();

//When a user press the enter key,send message.
msgBox.addEventListener('keydown', (e) => {
 if (e.keyCode === 13) {
   sendMessage({ email: email.value, text: e.target.value });
   e.target.value = '';
 }
});

//Display messages to the users
function loadDate(data) {
 let messages = '';
 data.map((message) => {
   messages += ` <li class="bg-primary p-2 rounded mb-2 text-light">
      <span class="fw-bolder">${message.email}</span>
      ${message.text}
    </li>`;
 });
 msgCont.innerHTML = messages;
}

//socket.io
//emit sendMessage event to send message
function sendMessage(message) {
 socket.emit('sendMessage', message);
}
//Listen to recMessage event to get the messages sent by users
socket.on('recMessage', (message) => {
 messages.push(message);
 loadDate(messages);
})
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, We created a socket.io instance and listened to the events on our server to send and receive a message from the server. We want the old chats to be available when a user joins the chat by default. Our application should look like the screenshot below:

Viewing user data with Arctype

We have now successfully created our chat application. First, let's look at the users' data with Arctype. To begin, launch Arctype, click the MySQL tab, and enter the following MySQL credentials, as shown in the screenshot below:

Then, click on the chattable to show the user’s chat messages, as shown in the screenshot below:

Testing the application

Now open the application in two different tabs or windows and try sending a message with a different email address as shown in the screenshot below:

Also, when you look at your console, you’d see logs when a user joins and disconnects from the server which is handled by the handleDisconnect and handleConnection methods.

Conclusion

Throughout this tutorial, we have explored how to create a real-time chat application with Nestjs and PostgreSQL. We started with a brief intro to Nestjs and WebSockets. Then we created a demo application to demonstrate the implementation. Hope you got the knowledge you seek. Perhaps you can learn more about WebSocket implementation from the Nestjs documentation and add extend the application.++

Top comments (2)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Nice and very good technical stack.

Collapse
 
root9464 profile image
iO

thank you very much for providing the source code, as a novice backend developer I am very grateful to you