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:
- Arctype
- NodeJS
- PostgreSQL
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
Creating the Project Folder
Then install the NestJS CLI with the command below:
npm i -g @nestjs/cli
When the installation is complete, run the command below to scaffold a NestJS project.
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:
npm i --save @nestjs/websockets @nestjs/platform-socket.io
Then, create a gateway application with the command below:
nest g gateway app
Now let's start our server by running the command below:
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.
sudo su - postgres
Then, create a new user account with the command below.
createuser --interactive
Next, create a new database. You can do that with the following command:
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[]:
...
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:
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.
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.
@WebSocketGateway({
cors: {
origin: '*',
},
})
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
}
}
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:
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:
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:
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:
<!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:
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.++