Build a Real-Time Chat App 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, WebSockets 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:

Shell
 
mkdir chatapp && cd chatapp


Creating the Project Folder

Then install the NestJS CLI with the command below:

Shell
 
npm i -g @nestjs/cli


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

Shell
 
nest new chat


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:

Shell
 
npm i --save @nestjs/websockets @nestjs/platform-socket.io 


Then, create a gateway application with the command below:

Shell
 
nest g gateway app


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

Shell
 
npm run start:dev 


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.

Shell
 
sudo su - postgres


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

Shell
 
createuser --interactive


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

Shell
 
createdb chat


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[]:

Shell
 
...
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]),
 ],
...


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 <USERNAME> and <PASSWORD> 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:

TypeScript
 
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;
}


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 WebSocket

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

TypeScript
 
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';


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.

TypeScript
 
@WebSocketGateway({
 cors: {
   origin: '*',
 },
})


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

TypeScript
 
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
 }
}


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 app.service.ts file and update the content with the code snippet below:

TypeScript
 
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();
 }
}


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

TypeScript
 
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);
 }
}


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:

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


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:

HTML
 
<!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>


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:

JavaScript
 
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);
})


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