NestJS is a popular framework for building server-side applications with Node.js. With its support for WebSockets, NestJS is well-suited for developing real-time chat applications.
So, what are WebSockets, and how can you build a real-time chat app in NestJS?
What Are WebSockets?
WebSockets are a protocol for persistent, real-time, and two-way communication between a client and a server.
Unlike in HTTP where a connection is closed when a request cycle is completed between the client and server, a WebSocket connection is kept open and doesn’t close up even after a response has been returned for a request.
The image below is a visualization of how a WebSocket communication between a server and client works:
To establish bidirectional communication, the client sends a WebSocket handshake request to the server. The request headers contain a secure WebSocket key (Sec-WebSocket-Key), and an Upgrade: WebSocket header which together with the Connection: Upgrade header tells the server to upgrade the protocol from HTTP to WebSocket, and keep the connection open. Learning about WebSockets in JavaScript helps to understand the concept even better.
Building a Real-Time Chat API Using the NestJS WebSocket Module
Node.js provides two major WebSockets implementations. The first is ws which implements bare WebSockets. And the second one is socket.io, which provides more high-level features.
NestJS has modules for both socket.io and ws. This article uses the socket.io module for the sample application's WebSocket features.
The code used in this project is available in a GitHub repository. It is recommended that you clone it locally to better understand the directory structure and see how all the codes interact with each other.
Project Setup and Installation
Open your terminal and generate a new NestJS app using the nest new command (e.g. nest new chat-app). The command generates a new directory that contains the project files. Now you're ready to start the development process.
Set Up a MongoDB Connection
To persist the chat messages in the application, you need a database. This article uses the MongoDB database for our NestJS application, and the easiest way to get running is to set up a MongoDB cluster in the cloud and get your MongoDB URL. Copy the URL and store it as the MONGO_URI variable in your .env file.
You would also be needing Mongoose later on when you make queries to MongoDB. Install it by running npm install mongoose in your terminal.
In the src folder, create a file called mongo.config.ts and paste the following code into it.
import { registerAs } from '@nestjs/config';
/**
* Mongo database connection config
*/
export default registerAs('mongodb', () => {
const { MONGO_URI } = process.env; // from .env file
return {
uri:`${MONGO_URI}`,
};
});
Your project's main.ts file should look like this:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser'
import helmet from 'helmet'
import { Logger, ValidationPipe } from '@nestjs/common';
import { setupSwagger } from './utils/swagger';
import { HttpExceptionFilter } from './filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
app.enableCors({
origin: '*',
credentials: true
})
app.use(cookieParser())
app.useGlobalPipes(
new ValidationPipe({
whitelist: true
})
)
const logger = new Logger('Main')
app.setGlobalPrefix('api/v1')
app.useGlobalFilters(new HttpExceptionFilter());
setupSwagger(app)
app.use(helmet())
await app.listen(AppModule.port)
// log docs
const baseUrl = AppModule.getBaseUrl(app)
const url = `http://${baseUrl}:${AppModule.port}`
logger.log(`API Documentation available at ${url}/docs`);
}
bootstrap();
Building the Chat Module
To get started with the real-time chat feature, the first step is to install the NestJS WebSockets packages. This can be done by running the following command in the terminal.
npm install @nestjs/websockets @nestjs/platform-socket.io @types/socket.io
After installing the packages, you need to generate the chats module by running the following commands
nest g module chats
nest g controller chats
nest g service chats
Once done generating the module, the next step is to create a WebSockets connection in NestJS. Create a chat.gateway.ts file inside the chats folder, this is where the gateway that sends and receives messages is implemented.
Paste the following code into chat.gateway.ts.
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
@WebSocketGateway()
export class ChatGateway {
@WebSocketServer()
server: Server;
// listen for send_message events
@SubscribeMessage('send_message')
listenForMessages(@MessageBody() message: string) {
this.server.sockets.emit('receive_message', message);
}
}
Authenticating Connected Users
Authentication is an essential part of web applications, and it's no different for a chat application. The function to authenticate client connections to the socket is found in chats.service.ts as shown here:
@Injectable()
export class ChatsService {
constructor(private authService: AuthService) {}
async getUserFromSocket(socket: Socket) {
let auth_token = socket.handshake.headers.authorization;
// get the token itself without "Bearer"
auth_token = auth_token.split(' ')[1];
const user = this.authService.getUserFromAuthenticationToken(
auth_token
);
if (!user) {
throw new WsException('Invalid credentials.');
}
return user;
}
}
The getUserFromSocket method uses getUserFromAuthenticationToken to get the currently logged-in user from the JWT token by extracting the Bearer token. The getUserFromAuthenticationToken function is implemented in the auth.service.ts file as shown here:
public async getUserFromAuthenticationToken(token: string) {
const payload: JwtPayload = this.jwtService.verify(token, {
secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
});
const userId = payload.sub
if (userId) {
return this.usersService.findById(userId);
}
}
The current socket is passed as a parameter to getUserFromSocket when the handleConnection method of ChatGateway implements the OnGatewayConnection interface. This makes it possible to receive messages and information about the currently connected user.
The code below demonstrates this:
// chat.gateway.ts
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;
constructor(private chatsService: ChatsService) {}
async handleConnection(socket: Socket) {
await this.chatsService.getUserFromSocket(socket)
}
@SubscribeMessage('send_message')
async listenForMessages(@MessageBody() message: string, @ConnectedSocket() socket: Socket) {
const user = await this.chatsService.getUserFromSocket(socket)
this.server.sockets.emit('receive_message', {
message,
user
});
}
}
You may reference the files involved in the authentication system above in the GitHub repository to see the complete codes (including imports), for a better understanding of the implementation.
Persisting Chats to Database
For users to see their messaging history, you need a schema to store messages. Create a new file called message.schema.ts and paste the code below into it (remember to import your user schema or check out the repository for one).
import { User } from './../users/schemas/user.schema';
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import mongoose, { Document } from "mongoose";
export type MessageDocument = Message & Document;
@Schema({
toJSON: {
getters: true,
virtuals: true,
},
timestamps: true,
})
export class Message {
@Prop({ required: true, unique: true })
message: string
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
user: User
}
const MessageSchema = SchemaFactory.createForClass(Message)
export { MessageSchema };
Below is an implementation of services to create a new message and get all messages in chats.service.ts.
import { Message, MessageDocument } from './message.schema';
import { Socket } from 'socket.io';
import { AuthService } from './../auth/auth.service';
import { Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { MessageDto } from './dto/message.dto';
@Injectable()
export class ChatsService {
constructor(private authService: AuthService, @InjectModel(Message.name) private messageModel: Model<MessageDocument>) {}
....
async createMessage(message: MessageDto, userId: string) {
const newMessage = new this.messageModel({...message, userId})
await newMessage.save
return newMessage
}
async getAllMessages() {
return this.messageModel.find().populate('user')
}
}
The MessageDto is implemented in a message.dto.ts file in the dto folder in the chats directory. You can also find it in the repository.
You need to add the message model and schema to the list of imports in chats.module.ts.
import { Message, MessageSchema } from './message.schema';
import { Module } from '@nestjs/common';
import { ChatGateway } from './chats.gateway';
import { ChatsService } from './chats.service';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forFeature([
{ name: Message.name, schema: MessageSchema }
])],
controllers: [],
providers: [ChatsService, ChatGateway]
})
export class ChatsModule {}
Finally, the get_all_messages events handler is added to the ChatGateway class in chat.gateway.ts as seen in the following code:
// imports...
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
....
@SubscribeMessage('get_all_messages')
async getAllMessages(@ConnectedSocket() socket: Socket) {
await this.chatsService.getUserFromSocket(socket)
const messages = await this.chatsService.getAllMessages()
this.server.sockets.emit('receive_message', messages);
return messages
}
}
When a connected client (user) emits the get_all_messages event, all their messages will be retrieved, and when they emit send_message, a message is created and stored in the database, and then sent to all other connected clients.
Once done with all the above steps, you may start your application using npm run start:dev, and test it with a WebSocket client like Postman.
Building Real-Time Applications With NestJS
Although there are other technologies for building real-time systems, WebSockets are very popular and easy to implement in many cases, and they are the best option for chat applications.
Real-time applications aren't only limited to chat applications, other examples include video streaming or calling applications, and live weather applications, and NestJS provides great tooling for building real-time apps.