DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on

How to create a full stack Chat App using Next js & Nest js?

Comprehensive Documentation for a Chat Application

Introduction

  • Purpose: Outline the purpose of the document.
  • Scope: Define the scope of the chat application.
  • Technologies Used: List Next.js, NestJS, TailwindCSS, REST API, WebSocket, MongoDB.

Project Structure

  • Frontend: Overview of the Next.js project structure.
  • Backend: Overview of the NestJS project structure.
  • Database: Structure of MongoDB collections (User, Chat, Message).

Sections and Functionality

1. User Authentication

  • User Registration:

    • Endpoint: /auth/register
    • Method: POST
    • Payload: { "username": "string", "password": "string" }
    • Description: Registers a new user.
  • User Login:

    • Endpoint: /auth/login
    • Method: POST
    • Payload: { "username": "string", "password": "string" }
    • Description: Authenticates a user and returns a JWT token.
  • User Logout:

    • Description: Logout mechanism (typically handled on the client side by destroying the JWT token).

2. User Management

  • Profile Management:

    • Endpoint: /users/me
    • Method: GET
    • Description: Fetches the logged-in user's profile.
  • Update Profile:

    • Endpoint: /users/me
    • Method: PUT
    • Payload: { "username": "string", "password": "string" }
    • Description: Updates the logged-in user's profile information.

3. Chat Management

  • Create Chat:

    • Endpoint: /chats
    • Method: POST
    • Payload: { "participants": ["userId1", "userId2"] }
    • Description: Creates a new chat session between users.
  • Fetch Chats:

    • Endpoint: /chats
    • Method: GET
    • Description: Fetches all chat sessions for the logged-in user.
  • Fetch Chat Details:

    • Endpoint: /chats/:chatId
    • Method: GET
    • Description: Fetches messages in a specific chat session.

4. Messaging

  • Send Message:

    • Endpoint: /chats/:chatId/messages
    • Method: POST
    • Payload: { "content": "string" }
    • Description: Sends a new message in a chat session.
  • Receive Messages:

    • Description: Real-time message receiving using WebSocket.
    • WebSocket Event: receiveMessage

5. Real-time Communication

  • WebSocket Setup:

    • Description: Initializing WebSocket connection on the client-side and handling events.
  • WebSocket Events:

    • sendMessage: Event to send a message.
    • receiveMessage: Event to receive messages.

6. User Interface

  • Login Page:

    • Description: UI for user login.
    • Components: Form, Input Fields, Submit Button.
  • Registration Page:

    • Description: UI for user registration.
    • Components: Form, Input Fields, Submit Button.
  • Chat List Page:

    • Description: UI for displaying the list of chat sessions.
    • Components: List of Chats, Search Bar.
  • Chat Window:

    • Description: UI for displaying chat messages and sending new messages.
    • Components: Message List, Input Field, Send Button.

7. Notifications

  • Real-time Notifications:
    • Description: Display real-time notifications for new messages.

8. File Sharing

  • Upload File:

    • Endpoint: /chats/:chatId/files
    • Method: POST
    • Payload: { "file": "file object" }
    • Description: Uploads a file to a chat session.
  • Download File:

    • Endpoint: /chats/:chatId/files/:fileId
    • Method: GET
    • Description: Downloads a file from a chat session.

9. Settings

  • User Settings:
    • Description: Page for user settings (e.g., notification preferences, account management).

10. Deployment

  • Frontend Deployment:

    • Description: Steps to deploy the Next.js app using Vercel.
  • Backend Deployment:

    • Description: Steps to deploy the NestJS app using Heroku, DigitalOcean, or AWS.

11. Security

  • JWT Authentication:

    • Description: Implementing JWT for user authentication.
  • Data Encryption:

    • Description: Encrypting sensitive data (e.g., passwords).

12. Testing

  • Unit Testing:

    • Description: Writing unit tests for both frontend and backend.
  • Integration Testing:

    • Description: Writing integration tests to test API endpoints.

13. Performance Optimization

  • Frontend Optimization:

    • Description: Techniques for optimizing the Next.js application (e.g., code splitting, lazy loading).
  • Backend Optimization:

    • Description: Techniques for optimizing the NestJS application (e.g., caching, database indexing).

14. Documentation

  • API Documentation:

    • Description: Detailed documentation of all API endpoints using tools like Swagger.
  • User Guide:

    • Description: Guide for end-users on how to use the chat application.

15. Future Enhancements

  • Voice and Video Calls:

    • Description: Adding support for voice and video calls.
  • Group Chats:

    • Description: Adding support for group chat functionality.
  • Status Indicators:

    • Description: Adding online/offline status indicators for users.

This comprehensive breakdown covers the essential sections and functionalities needed to create a chat application similar to WhatsApp or Telegram using Next.js, NestJS, and TailwindCSS.

Creating a chat application like WhatsApp or Telegram involves a comprehensive set of features and technologies. Below is a detailed breakdown to help you get started with building a chat app using Next.js for the frontend, NestJS for the backend, TailwindCSS for styling, and REST APIs for communication.

1. Project Setup

Frontend (Next.js)

  • Initialize Next.js:
  npx create-next-app@latest chat-app
  cd chat-app
Enter fullscreen mode Exit fullscreen mode
  • Install TailwindCSS:
  npm install -D tailwindcss postcss autoprefixer
  npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode
  • Configure TailwindCSS: Update tailwind.config.js and globals.css.
  // tailwind.config.js
  module.exports = {
    content: [
      "./pages/**/*.{js,ts,jsx,tsx}",
      "./components/**/*.{js,ts,jsx,tsx}",
    ],
    theme: {
      extend: {},
    },
    plugins: [],
  };
Enter fullscreen mode Exit fullscreen mode
  /* globals.css */
  @tailwind base;
  @tailwind components;
  @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Backend (NestJS)

  • Initialize NestJS:
  npm i -g @nestjs/cli
  nest new chat-backend
  cd chat-backend
Enter fullscreen mode Exit fullscreen mode
  • Install Required Modules:
  npm install @nestjs/mongoose mongoose @nestjs/passport passport passport-local bcryptjs
  npm install --save-dev @types/passport-local
Enter fullscreen mode Exit fullscreen mode

2. Database Schema and Models

  • Define User and Chat Models: Use Mongoose for schema definition.

User Schema

import { Schema } from 'mongoose';

export const UserSchema = new Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});
Enter fullscreen mode Exit fullscreen mode

Chat Schema

import { Schema } from 'mongoose';

export const ChatSchema = new Schema({
  participants: [{ type: Schema.Types.ObjectId, ref: 'User' }],
  messages: [
    {
      sender: { type: Schema.Types.ObjectId, ref: 'User' },
      content: { type: String, required: true },
      timestamp: { type: Date, default: Date.now },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

3. Authentication

  • Local Strategy for Authentication: Use Passport.js for authentication.

Local Strategy

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

4. REST API Endpoints

  • User Registration and Login: Implement endpoints for user registration and login.

Auth Controller

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('register')
  async register(@Request() req) {
    return this.authService.register(req.body);
  }

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Chat Functionality

  • Create and Fetch Chats: Implement endpoints for creating and fetching chats and messages.

Chat Controller

import { Controller, Post, Get, Param, Body } from '@nestjs/common';
import { ChatService } from './chat.service';

@Controller('chats')
export class ChatController {
  constructor(private chatService: ChatService) {}

  @Post()
  async createChat(@Body() createChatDto: CreateChatDto) {
    return this.chatService.createChat(createChatDto);
  }

  @Get(':chatId')
  async getChat(@Param('chatId') chatId: string) {
    return this.chatService.getChat(chatId);
  }

  @Post(':chatId/messages')
  async sendMessage(@Param('chatId') chatId: string, @Body() sendMessageDto: SendMessageDto) {
    return this.chatService.sendMessage(chatId, sendMessageDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Real-time Communication

  • WebSocket for Real-Time: Integrate WebSocket for real-time messaging.

Install Dependencies

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

WebSocket Gateway

import {
  SubscribeMessage,
  WebSocketGateway,
  OnGatewayInit,
  WebSocketServer,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server;

  handleConnection(client: Socket, ...args: any[]) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }

  @SubscribeMessage('sendMessage')
  handleMessage(client: Socket, payload: any): void {
    this.server.emit('receiveMessage', payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Frontend Integration

  • Real-Time Messaging with Socket.io: Use Socket.io on the frontend for real-time updates.

Install Socket.io Client

npm install socket.io-client
Enter fullscreen mode Exit fullscreen mode

Frontend Integration

import { useEffect, useState } from 'react';
import io from 'socket.io-client';

const socket = io('http://localhost:3000');

export default function Chat() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    socket.on('receiveMessage', (message) => {
      setMessages((prevMessages) => [...prevMessages, message]);
    });
  }, []);

  const sendMessage = () => {
    socket.emit('sendMessage', input);
    setInput('');
  };

  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map((msg, index) => (
          <div key={index}>{msg}</div>
        ))}
      </div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => (e.key === 'Enter' ? sendMessage() : null)}
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

8. Styling with TailwindCSS

  • TailwindCSS for UI: Style the chat interface using TailwindCSS.

Example Styles

// Example chat component with TailwindCSS classes
export default function Chat() {
  // ... (useState and useEffect hooks)

  return (
    <div className="flex flex-col h-screen">
      <div className="flex-1 overflow-y-auto p-4">
        {messages.map((msg, index) => (
          <div key={index} className="bg-gray-200 p-2 my-2 rounded">
            {msg}
          </div>
        ))}
      </div>
      <div className="p-4 border-t border-gray-300 flex">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="flex-1 p-2 border rounded"
          onKeyDown={(e) => (e.key === 'Enter' ? sendMessage() : null)}
        />
        <button
          onClick={sendMessage}
          className="ml-2 bg-blue-500 text-white p-2 rounded"
        >
          Send
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

9. Deployment and Hosting

  • Frontend Deployment: Use Vercel for deploying the Next.js app.
  • Backend Deployment: Use services like Heroku, DigitalOcean, or AWS for deploying the NestJS app.

By following this breakdown, you can build a comprehensive chat application with real-time messaging capabilities, user authentication, and a responsive UI. Adjust and expand upon these basics to include additional features like user profiles, file sharing, notifications, etc., as needed.

User Authentication: User Registration

Backend Code (NestJS)

  1. Auth Module Setup:

    • Generate the Auth Module:
     nest generate module auth
     nest generate service auth
     nest generate controller auth
    
  2. User Schema:

    • Create User Schema (src/schemas/user.schema.ts):
     import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
     import { Document } from 'mongoose';
    
     export type UserDocument = User & Document;
    
     @Schema()
     export class User {
       @Prop({ required: true, unique: true })
       username: string;
    
       @Prop({ required: true })
       password: string;
     }
    
     export const UserSchema = SchemaFactory.createForClass(User);
    
  3. User DTO (Data Transfer Object):

    • Create DTO for User Registration (src/auth/dto/register-user.dto.ts):
     export class RegisterUserDto {
       username: string;
       password: string;
     }
    
  4. Auth Service:

    • Update the Auth Service (src/auth/auth.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { User, UserDocument } from '../schemas/user.schema';
     import { RegisterUserDto } from './dto/register-user.dto';
     import * as bcrypt from 'bcrypt';
    
     @Injectable()
     export class AuthService {
       constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
    
       async register(registerUserDto: RegisterUserDto): Promise<User> {
         const { username, password } = registerUserDto;
    
         // Check if the user already exists
         const existingUser = await this.userModel.findOne({ username }).exec();
         if (existingUser) {
           throw new Error('User already exists');
         }
    
         // Hash the password
         const salt = await bcrypt.genSalt();
         const hashedPassword = await bcrypt.hash(password, salt);
    
         // Create a new user
         const newUser = new this.userModel({ username, password: hashedPassword });
         return newUser.save();
       }
     }
    
  5. Auth Controller:

    • Update the Auth Controller (src/auth/auth.controller.ts):
     import { Controller, Post, Body } from '@nestjs/common';
     import { AuthService } from './auth.service';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { User } from '../schemas/user.schema';
    
     @Controller('auth')
     export class AuthController {
       constructor(private readonly authService: AuthService) {}
    
       @Post('register')
       async register(@Body() registerUserDto: RegisterUserDto): Promise<User> {
         return this.authService.register(registerUserDto);
       }
     }
    
  6. Mongoose Setup:

    • Configure Mongoose in App Module (src/app.module.ts):
     import { Module } from '@nestjs/common';
     import { MongooseModule } from '@nestjs/mongoose';
     import { AuthModule } from './auth/auth.module';
     import { User, UserSchema } from './schemas/user.schema';
    
     @Module({
       imports: [
         MongooseModule.forRoot('mongodb://localhost/chat-app'),
         MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
         AuthModule,
       ],
     })
     export class AppModule {}
    

Frontend Code (Next.js)

  1. API Call for Registration:

    • Create API Function (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000/auth';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
  2. Registration Form:

    • Create Registration Form Component (src/components/RegisterForm.js):
     import { useState } from 'react';
     import { register } from '../services/api';
    
     export default function RegisterForm() {
       const [username, setUsername] = useState('');
       const [password, setPassword] = useState('');
       const [message, setMessage] = useState('');
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           const data = await register(username, password);
           setMessage('User registered successfully');
         } catch (error) {
           setMessage(`Error: ${error.message}`);
         }
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Register</h2>
           <form onSubmit={handleSubmit}>
             <div className="mb-4">
               <label className="block text-gray-700">Username</label>
               <input
                 type="text"
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
               />
             </div>
             <div className="mb-4">
               <label className="block text-gray-700">Password</label>
               <input
                 type="password"
                 value={password}
                 onChange={(e) => setPassword(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
               />
             </div>
             <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
               Register
             </button>
           </form>
           {message && <p className="mt-4">{message}</p>}
         </div>
       );
     }
    

Conclusion

This setup includes the backend implementation for user registration with NestJS and a simple frontend registration form with Next.js. It uses MongoDB for data storage and bcrypt for password hashing. Adjust and expand this as needed for your complete chat application.

User Authentication: User Login and Logout

Backend Code (NestJS)

  1. Auth Module Setup (continued from the previous setup)

  2. Login DTO (Data Transfer Object):

    • Create DTO for User Login (src/auth/dto/login-user.dto.ts):
     export class LoginUserDto {
       username: string;
       password: string;
     }
    
  3. JWT Module Setup:

    • Install JWT Package:
     npm install @nestjs/jwt passport-jwt
     npm install --save-dev @types/passport-jwt
    
  • Configure JWT in Auth Module (src/auth/auth.module.ts):

     import { Module } from '@nestjs/common';
     import { JwtModule } from '@nestjs/jwt';
     import { PassportModule } from '@nestjs/passport';
     import { AuthService } from './auth.service';
     import { AuthController } from './auth.controller';
     import { User, UserSchema } from '../schemas/user.schema';
     import { MongooseModule } from '@nestjs/mongoose';
     import { JwtStrategy } from './jwt.strategy';
    
     @Module({
       imports: [
         MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
         PassportModule,
         JwtModule.register({
           secret: 'YOUR_SECRET_KEY', // Replace with a secure key
           signOptions: { expiresIn: '1h' },
         }),
       ],
       providers: [AuthService, JwtStrategy],
       controllers: [AuthController],
     })
     export class AuthModule {}
    
  1. Auth Service (continued):

    • Update Auth Service to Handle Login and JWT Generation (src/auth/auth.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { JwtService } from '@nestjs/jwt';
     import { User, UserDocument } from '../schemas/user.schema';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import * as bcrypt from 'bcrypt';
    
     @Injectable()
     export class AuthService {
       constructor(
         @InjectModel(User.name) private userModel: Model<UserDocument>,
         private jwtService: JwtService,
       ) {}
    
       async register(registerUserDto: RegisterUserDto): Promise<User> {
         const { username, password } = registerUserDto;
    
         const existingUser = await this.userModel.findOne({ username }).exec();
         if (existingUser) {
           throw new Error('User already exists');
         }
    
         const salt = await bcrypt.genSalt();
         const hashedPassword = await bcrypt.hash(password, salt);
    
         const newUser = new this.userModel({ username, password: hashedPassword });
         return newUser.save();
       }
    
       async validateUser(username: string, password: string): Promise<User> {
         const user = await this.userModel.findOne({ username }).exec();
         if (!user) {
           return null;
         }
    
         const isPasswordValid = await bcrypt.compare(password, user.password);
         if (!isPasswordValid) {
           return null;
         }
    
         return user;
       }
    
       async login(loginUserDto: LoginUserDto): Promise<{ access_token: string }> {
         const { username, password } = loginUserDto;
         const user = await this.validateUser(username, password);
         if (!user) {
           throw new Error('Invalid credentials');
         }
    
         const payload = { username: user.username, sub: user._id };
         return {
           access_token: this.jwtService.sign(payload),
         };
       }
     }
    
  2. Auth Controller (continued):

    • Update Auth Controller for Login (src/auth/auth.controller.ts):
     import { Controller, Post, Body } from '@nestjs/common';
     import { AuthService } from './auth.service';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import { User } from '../schemas/user.schema';
    
     @Controller('auth')
     export class AuthController {
       constructor(private readonly authService: AuthService) {}
    
       @Post('register')
       async register(@Body() registerUserDto: RegisterUserDto): Promise<User> {
         return this.authService.register(registerUserDto);
       }
    
       @Post('login')
       async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> {
         return this.authService.login(loginUserDto);
       }
     }
    
  3. JWT Strategy:

    • Create JWT Strategy for Protecting Routes (src/auth/jwt.strategy.ts):
     import { Strategy, ExtractJwt } from 'passport-jwt';
     import { PassportStrategy } from '@nestjs/passport';
     import { Injectable } from '@nestjs/common';
     import { JwtPayload } from './jwt-payload.interface';
     import { AuthService } from './auth.service';
    
     @Injectable()
     export class JwtStrategy extends PassportStrategy(Strategy) {
       constructor(private authService: AuthService) {
         super({
           jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
           ignoreExpiration: false,
           secretOrKey: 'YOUR_SECRET_KEY', // Replace with a secure key
         });
       }
    
       async validate(payload: JwtPayload) {
         return { userId: payload.sub, username: payload.username };
       }
     }
    
  4. JWT Payload Interface:

    • Create JWT Payload Interface (src/auth/jwt-payload.interface.ts):
     export interface JwtPayload {
       username: string;
       sub: string;
     }
    

Frontend Code (Next.js)

  1. API Call for Login:

    • Update API Function (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000/auth';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
  2. Login Form:

    • Create Login Form Component (src/components/LoginForm.js):
     import { useState } from 'react';
     import { login } from '../services/api';
    
     export default function LoginForm() {
       const [username, setUsername] = useState('');
       const [password, setPassword] = useState('');
       const [message, setMessage] = useState('');
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           const data = await login(username, password);
           localStorage.setItem('token', data.access_token);
           setMessage('User logged in successfully');
         } catch (error) {
           setMessage(`Error: ${error.message}`);
         }
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Login</h2>
           <form onSubmit={handleSubmit}>
             <div className="mb-4">
               <label className="block text-gray-700">Username</label>
               <input
                 type="text"
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
               />
             </div>
             <div className="mb-4">
               <label className="block text-gray-700">Password</label>
               <input
                 type="password"
                 value={password}
                 onChange={(e) => setPassword(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
               />
             </div>
             <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
               Login
             </button>
           </form>
           {message && <p className="mt-4">{message}</p>}
         </div>
       );
     }
    
  3. Logout Function:

    • Handle Logout on the Client Side:
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  • Example Logout Button:

     import { logout } from '../services/api';
    
     export default function LogoutButton() {
       return (
         <button onClick={logout} className="bg-red-500 text-white px-4 py-2 rounded">
           Logout
         </button>
       );
     }
    

Conclusion

This setup includes the backend implementation for user login with NestJS and a simple frontend login form with Next.js. The logout mechanism is handled on the client side by removing the JWT token from local storage. This setup provides the basic authentication flow for a chat application.

User Management: Profile Management

Backend Code (NestJS)

  1. User Profile Endpoint

  2. Create User Profile DTO:

    • Create DTO for User Profile (src/auth/dto/user-profile.dto.ts):
     export class UserProfileDto {
       username: string;
     }
    
  3. User Service:

    • Update User Service to Handle Profile Fetching (src/auth/auth.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { JwtService } from '@nestjs/jwt';
     import { User, UserDocument } from '../schemas/user.schema';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import { UserProfileDto } from './dto/user-profile.dto';
     import * as bcrypt from 'bcrypt';
    
     @Injectable()
     export class AuthService {
       constructor(
         @InjectModel(User.name) private userModel: Model<UserDocument>,
         private jwtService: JwtService,
       ) {}
    
       // ... other methods
    
       async getUserProfile(userId: string): Promise<UserProfileDto> {
         const user = await this.userModel.findById(userId).exec();
         if (!user) {
           throw new Error('User not found');
         }
         return { username: user.username };
       }
     }
    
  4. Auth Controller:

    • Update Auth Controller to Include Profile Fetching (src/auth/auth.controller.ts):
     import { Controller, Post, Get, Request, Body, UseGuards } from '@nestjs/common';
     import { AuthService } from './auth.service';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import { User } from '../schemas/user.schema';
     import { JwtAuthGuard } from './jwt-auth.guard';
    
     @Controller('auth')
     export class AuthController {
       constructor(private readonly authService: AuthService) {}
    
       @Post('register')
       async register(@Body() registerUserDto: RegisterUserDto): Promise<User> {
         return this.authService.register(registerUserDto);
       }
    
       @Post('login')
       async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> {
         return this.authService.login(loginUserDto);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get('me')
       async getProfile(@Request() req): Promise<{ username: string }> {
         return this.authService.getUserProfile(req.user.userId);
       }
     }
    
  5. JWT Auth Guard:

    • Create JWT Auth Guard (src/auth/jwt-auth.guard.ts):
     import { Injectable } from '@nestjs/common';
     import { AuthGuard } from '@nestjs/passport';
    
     @Injectable()
     export class JwtAuthGuard extends AuthGuard('jwt') {}
    
  6. Update JWT Strategy:

    • Ensure Payload Includes User ID (src/auth/jwt.strategy.ts):
     import { Strategy, ExtractJwt } from 'passport-jwt';
     import { PassportStrategy } from '@nestjs/passport';
     import { Injectable } from '@nestjs/common';
     import { JwtPayload } from './jwt-payload.interface';
     import { AuthService } from './auth.service';
    
     @Injectable()
     export class JwtStrategy extends PassportStrategy(Strategy) {
       constructor(private authService: AuthService) {
         super({
           jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
           ignoreExpiration: false,
           secretOrKey: 'YOUR_SECRET_KEY', // Replace with a secure key
         });
       }
    
       async validate(payload: JwtPayload) {
         return { userId: payload.sub, username: payload.username };
       }
     }
    

Frontend Code (Next.js)

  1. API Call for Fetching User Profile:

    • Create API Function for Fetching User Profile (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000/auth';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Profile Page:

    • Create Profile Page Component (src/pages/profile.js):
     import { useEffect, useState } from 'react';
     import { getProfile } from '../services/api';
    
     export default function Profile() {
       const [profile, setProfile] = useState(null);
       const [error, setError] = useState('');
    
       useEffect(() => {
         const fetchProfile = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getProfile(token);
               setProfile(data);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchProfile();
       }, []);
    
       if (error) {
         return <div className="text-red-500">{error}</div>;
       }
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Profile</h2>
           {profile ? (
             <div>
               <p><strong>Username:</strong> {profile.username}</p>
             </div>
           ) : (
             <p>Loading...</p>
           )}
         </div>
       );
     }
    

Conclusion

This setup includes the backend implementation for fetching the logged-in user's profile with NestJS and a simple frontend profile page with Next.js. The profile endpoint is protected with JWT authentication, ensuring only authenticated users can access their profile information.

User Management: Update Profile

Backend Code (NestJS)

  1. Update User Profile DTO:

    • Create DTO for Updating User Profile (src/auth/dto/update-user.dto.ts):
     export class UpdateUserDto {
       username?: string;
       password?: string;
     }
    
  2. User Service:

    • Update User Service to Handle Profile Updating (src/auth/auth.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { JwtService } from '@nestjs/jwt';
     import { User, UserDocument } from '../schemas/user.schema';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import { UpdateUserDto } from './dto/update-user.dto';
     import * as bcrypt from 'bcrypt';
    
     @Injectable()
     export class AuthService {
       constructor(
         @InjectModel(User.name) private userModel: Model<UserDocument>,
         private jwtService: JwtService,
       ) {}
    
       // ... other methods
    
       async updateUserProfile(userId: string, updateUserDto: UpdateUserDto): Promise<User> {
         const user = await this.userModel.findById(userId).exec();
         if (!user) {
           throw new Error('User not found');
         }
    
         if (updateUserDto.username) {
           user.username = updateUserDto.username;
         }
    
         if (updateUserDto.password) {
           const salt = await bcrypt.genSalt();
           user.password = await bcrypt.hash(updateUserDto.password, salt);
         }
    
         return user.save();
       }
     }
    
  3. Auth Controller:

    • Update Auth Controller to Include Profile Updating (src/auth/auth.controller.ts):
     import { Controller, Post, Get, Put, Request, Body, UseGuards } from '@nestjs/common';
     import { AuthService } from './auth.service';
     import { RegisterUserDto } from './dto/register-user.dto';
     import { LoginUserDto } from './dto/login-user.dto';
     import { UpdateUserDto } from './dto/update-user.dto';
     import { User } from '../schemas/user.schema';
     import { JwtAuthGuard } from './jwt-auth.guard';
    
     @Controller('auth')
     export class AuthController {
       constructor(private readonly authService: AuthService) {}
    
       @Post('register')
       async register(@Body() registerUserDto: RegisterUserDto): Promise<User> {
         return this.authService.register(registerUserDto);
       }
    
       @Post('login')
       async login(@Body() loginUserDto: LoginUserDto): Promise<{ access_token: string }> {
         return this.authService.login(loginUserDto);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get('me')
       async getProfile(@Request() req): Promise<{ username: string }> {
         return this.authService.getUserProfile(req.user.userId);
       }
    
       @UseGuards(JwtAuthGuard)
       @Put('me')
       async updateProfile(@Request() req, @Body() updateUserDto: UpdateUserDto): Promise<User> {
         return this.authService.updateUserProfile(req.user.userId, updateUserDto);
       }
     }
    
  4. JWT Auth Guard and Strategy (from previous steps).

Frontend Code (Next.js)

  1. API Call for Updating User Profile:

    • Create API Function for Updating User Profile (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000/auth';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Update Profile Form:

    • Create Update Profile Form Component (src/components/UpdateProfileForm.js):
     import { useState, useEffect } from 'react';
     import { getProfile, updateProfile } from '../services/api';
    
     export default function UpdateProfileForm() {
       const [username, setUsername] = useState('');
       const [password, setPassword] = useState('');
       const [message, setMessage] = useState('');
    
       useEffect(() => {
         const fetchProfile = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getProfile(token);
               setUsername(data.username);
             }
           } catch (err) {
             setMessage(`Error: ${err.message}`);
           }
         };
    
         fetchProfile();
       }, []);
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           const token = localStorage.getItem('token');
           const userData = { username, password: password || undefined };
           const data = await updateProfile(token, userData);
           setMessage('Profile updated successfully');
         } catch (error) {
           setMessage(`Error: ${error.message}`);
         }
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Update Profile</h2>
           <form onSubmit={handleSubmit}>
             <div className="mb-4">
               <label className="block text-gray-700">Username</label>
               <input
                 type="text"
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
               />
             </div>
             <div className="mb-4">
               <label className="block text-gray-700">Password</label>
               <input
                 type="password"
                 value={password}
                 onChange={(e) => setPassword(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
                 placeholder="Leave blank to keep the same"
               />
             </div>
             <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
               Update
             </button>
           </form>
           {message && <p className="mt-4">{message}</p>}
         </div>
       );
     }
    

Conclusion

This setup includes the backend implementation for updating the logged-in user's profile with NestJS and a simple frontend update profile form with Next.js. The profile update endpoint is protected with JWT authentication, ensuring only authenticated users can update their profile information.

Chat Management: Create Chat

Backend Code (NestJS)

  1. Chat Module Setup

    • Generate the Chat Module:
     nest generate module chat
     nest generate service chat
     nest generate controller chat
    
  2. Chat Schema:

    • Create Chat Schema (src/schemas/chat.schema.ts):
     import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
     import { Document, Types } from 'mongoose';
    
     export type ChatDocument = Chat & Document;
    
     @Schema()
     export class Chat {
       @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true })
       participants: Types.ObjectId[];
    
       @Prop({ type: [{ type: Object }] })
       messages: { sender: Types.ObjectId; content: string; timestamp: Date }[];
     }
    
     export const ChatSchema = SchemaFactory.createForClass(Chat);
    
  3. Create Chat DTO:

    • Create DTO for Creating Chat (src/chat/dto/create-chat.dto.ts):
     export class CreateChatDto {
       participants: string[];
     }
    
  4. Chat Service:

    • Update Chat Service to Handle Chat Creation (src/chat/chat.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { CreateChatDto } from './dto/create-chat.dto';
    
     @Injectable()
     export class ChatService {
       constructor(@InjectModel(Chat.name) private chatModel: Model<ChatDocument>) {}
    
       async createChat(createChatDto: CreateChatDto): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: createChatDto.participants,
           messages: [],
         });
         return newChat.save();
       }
     }
    
  5. Chat Controller:

    • Update Chat Controller to Include Chat Creation (src/chat/chat.controller.ts):
     import { Controller, Post, Body, UseGuards } from '@nestjs/common';
     import { ChatService } from './chat.service';
     import { CreateChatDto } from './dto/create-chat.dto';
     import { JwtAuthGuard } from '../auth/jwt-auth.guard';
     import { Chat } from '../schemas/chat.schema';
    
     @Controller('chats')
     export class ChatController {
       constructor(private readonly chatService: ChatService) {}
    
       @UseGuards(JwtAuthGuard)
       @Post()
       async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> {
         return this.chatService.createChat(createChatDto);
       }
     }
    
  6. JWT Auth Guard (from previous steps).

Frontend Code (Next.js)

  1. API Call for Creating Chat:

    • Create API Function for Creating Chat (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Create Chat Form:

    • Create Chat Form Component (src/components/CreateChatForm.js):
     import { useState } from 'react';
     import { createChat } from '../services/api';
    
     export default function CreateChatForm() {
       const [participants, setParticipants] = useState('');
       const [message, setMessage] = useState('');
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           const token = localStorage.getItem('token');
           const participantIds = participants.split(',').map(id => id.trim());
           const data = await createChat(token, participantIds);
           setMessage('Chat created successfully');
         } catch (error) {
           setMessage(`Error: ${error.message}`);
         }
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Create Chat</h2>
           <form onSubmit={handleSubmit}>
             <div className="mb-4">
               <label className="block text-gray-700">Participants</label>
               <input
                 type="text"
                 value={participants}
                 onChange={(e) => setParticipants(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
                 placeholder="Comma-separated user IDs"
               />
             </div>
             <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
               Create Chat
             </button>
           </form>
           {message && <p className="mt-4">{message}</p>}
         </div>
       );
     }
    

Conclusion

This setup includes the backend implementation for creating a new chat session with NestJS and a simple frontend form for creating a chat with Next.js. The chat creation endpoint is protected with JWT authentication, ensuring only authenticated users can create new chat sessions.

Chat Management: Fetch Chats

Backend Code (NestJS)

  1. Chat Service:

    • Update Chat Service to Handle Fetching Chats (src/chat/chat.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { User, UserDocument } from '../schemas/user.schema';
    
     @Injectable()
     export class ChatService {
       constructor(
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
         @InjectModel(User.name) private userModel: Model<UserDocument>,
       ) {}
    
       async createChat(participants: string[]): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: participants,
           messages: [],
         });
         return newChat.save();
       }
    
       async getChats(userId: string): Promise<Chat[]> {
         return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec();
       }
     }
    
  2. Chat Controller:

    • Update Chat Controller to Include Fetching Chats (src/chat/chat.controller.ts):
     import { Controller, Post, Get, Body, UseGuards, Request } from '@nestjs/common';
     import { ChatService } from './chat.service';
     import { CreateChatDto } from './dto/create-chat.dto';
     import { JwtAuthGuard } from '../auth/jwt-auth.guard';
     import { Chat } from '../schemas/chat.schema';
    
     @Controller('chats')
     export class ChatController {
       constructor(private readonly chatService: ChatService) {}
    
       @UseGuards(JwtAuthGuard)
       @Post()
       async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> {
         return this.chatService.createChat(createChatDto.participants);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get()
       async getChats(@Request() req): Promise<Chat[]> {
         return this.chatService.getChats(req.user.userId);
       }
     }
    
  3. Ensure JWT Auth Guard is Applied:

    • Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

Frontend Code (Next.js)

  1. API Call for Fetching Chats:

    • Create API Function for Fetching Chats (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Chat List Page:

    • Create Chat List Component (src/components/ChatList.js):
     import { useEffect, useState } from 'react';
     import { getChats } from '../services/api';
    
     export default function ChatList() {
       const [chats, setChats] = useState([]);
       const [error, setError] = useState('');
    
       useEffect(() => {
         const fetchChats = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getChats(token);
               setChats(data);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchChats();
       }, []);
    
       if (error) {
         return <div className="text-red-500">{error}</div>;
       }
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chats</h2>
           {chats.length > 0 ? (
             <ul>
               {chats.map((chat) => (
                 <li key={chat._id} className="mb-2 p-2 border rounded">
                   {chat.participants.map((participant) => participant.username).join(', ')}
                 </li>
               ))}
             </ul>
           ) : (
             <p>No chats available.</p>
           )}
         </div>
       );
     }
    

Conclusion

This setup includes the backend implementation for fetching all chat sessions for the logged-in user with NestJS and a simple frontend chat list component with Next.js. The chat fetching endpoint is protected with JWT authentication, ensuring only authenticated users can fetch their chat sessions.

Chat Management: Fetch Chat Details

Backend Code (NestJS)

  1. Chat Service:

    • Update Chat Service to Handle Fetching Chat Details (src/chat/chat.service.ts):
     import { Injectable } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { User, UserDocument } from '../schemas/user.schema';
    
     @Injectable()
     export class ChatService {
       constructor(
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
         @InjectModel(User.name) private userModel: Model<UserDocument>,
       ) {}
    
       async createChat(participants: string[]): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: participants,
           messages: [],
         });
         return newChat.save();
       }
    
       async getChats(userId: string): Promise<Chat[]> {
         return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec();
       }
    
       async getChatDetails(chatId: string): Promise<Chat> {
         return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec();
       }
     }
    
  2. Chat Controller:

    • Update Chat Controller to Include Fetching Chat Details (src/chat/chat.controller.ts):
     import { Controller, Post, Get, Param, Body, UseGuards, Request } from '@nestjs/common';
     import { ChatService } from './chat.service';
     import { CreateChatDto } from './dto/create-chat.dto';
     import { JwtAuthGuard } from '../auth/jwt-auth.guard';
     import { Chat } from '../schemas/chat.schema';
    
     @Controller('chats')
     export class ChatController {
       constructor(private readonly chatService: ChatService) {}
    
       @UseGuards(JwtAuthGuard)
       @Post()
       async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> {
         return this.chatService.createChat(createChatDto.participants);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get()
       async getChats(@Request() req): Promise<Chat[]> {
         return this.chatService.getChats(req.user.userId);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get(':chatId')
       async getChatDetails(@Param('chatId') chatId: string): Promise<Chat> {
         return this.chatService.getChatDetails(chatId);
       }
     }
    
  3. Ensure JWT Auth Guard is Applied:

    • Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

Frontend Code (Next.js)

  1. API Call for Fetching Chat Details:

    • Create API Function for Fetching Chat Details (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Chat Details Page:

    • Create Chat Details Component (src/components/ChatDetails.js):
     import { useEffect, useState } from 'react';
     import { getChatDetails } from '../services/api';
    
     export default function ChatDetails({ chatId }) {
       const [chat, setChat] = useState(null);
       const [error, setError] = useState('');
    
       useEffect(() => {
         const fetchChatDetails = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getChatDetails(token, chatId);
               setChat(data);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchChatDetails();
       }, [chatId]);
    
       if (error) {
         return <div className="text-red-500">{error}</div>;
       }
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat Details</h2>
           {chat ? (
             <div>
               <h3 className="text-xl font-bold mb-3">Participants</h3>
               <ul>
                 {chat.participants.map((participant) => (
                   <li key={participant._id}>{participant.username}</li>
                 ))}
               </ul>
               <h3 className="text-xl font-bold mb-3 mt-5">Messages</h3>
               <ul>
                 {chat.messages.map((message) => (
                   <li key={message._id} className="mb-2 p-2 border rounded">
                     <strong>{message.sender.username}</strong>: {message.content} <br />
                     <span className="text-gray-500 text-sm">{new Date(message.timestamp).toLocaleString()}</span>
                   </li>
                 ))}
               </ul>
             </div>
           ) : (
             <p>Loading...</p>
           )}
         </div>
       );
     }
    
  3. Usage in a Page:

    • Create a Page to Display Chat Details (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatDetails from '../../components/ChatDetails';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatDetails chatId={chatId} />;
     }
    

Conclusion

This setup includes the backend implementation for fetching details of a specific chat session with NestJS and a simple frontend chat details component with Next.js. The chat details endpoint is protected with JWT authentication, ensuring only authenticated users can fetch the details of their chat sessions.

Messaging: Send Message

Backend Code (NestJS)

  1. Message DTO:

    • Create DTO for Sending Message (src/chat/dto/send-message.dto.ts):
     export class SendMessageDto {
       content: string;
     }
    
  2. Chat Schema Update:

    • Update Chat Schema to Include Messages (src/schemas/chat.schema.ts):
     import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
     import { Document, Types } from 'mongoose';
    
     export type ChatDocument = Chat & Document;
    
     @Schema()
     export class Chat {
       @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true })
       participants: Types.ObjectId[];
    
       @Prop({ type: [{ sender: { type: Types.ObjectId, ref: 'User' }, content: String, timestamp: Date }], default: [] })
       messages: { sender: Types.ObjectId; content: string; timestamp: Date }[];
     }
    
     export const ChatSchema = SchemaFactory.createForClass(Chat);
    
  3. Chat Service:

    • Update Chat Service to Handle Sending Messages (src/chat/chat.service.ts):
     import { Injectable, NotFoundException } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { User, UserDocument } from '../schemas/user.schema';
     import { SendMessageDto } from './dto/send-message.dto';
    
     @Injectable()
     export class ChatService {
       constructor(
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
         @InjectModel(User.name) private userModel: Model<UserDocument>,
       ) {}
    
       async createChat(participants: string[]): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: participants,
           messages: [],
         });
         return newChat.save();
       }
    
       async getChats(userId: string): Promise<Chat[]> {
         return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec();
       }
    
       async getChatDetails(chatId: string): Promise<Chat> {
         return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec();
       }
    
       async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> {
         const chat = await this.chatModel.findById(chatId);
         if (!chat) {
           throw new NotFoundException('Chat not found');
         }
    
         chat.messages.push({
           sender: userId,
           content: sendMessageDto.content,
           timestamp: new Date(),
         });
    
         return chat.save();
       }
     }
    
  4. Chat Controller:

    • Update Chat Controller to Include Sending Messages (src/chat/chat.controller.ts):
     import { Controller, Post, Get, Param, Body, UseGuards, Request } from '@nestjs/common';
     import { ChatService } from './chat.service';
     import { CreateChatDto } from './dto/create-chat.dto';
     import { SendMessageDto } from './dto/send-message.dto';
     import { JwtAuthGuard } from '../auth/jwt-auth.guard';
     import { Chat } from '../schemas/chat.schema';
    
     @Controller('chats')
     export class ChatController {
       constructor(private readonly chatService: ChatService) {}
    
       @UseGuards(JwtAuthGuard)
       @Post()
       async createChat(@Body() createChatDto: CreateChatDto): Promise<Chat> {
         return this.chatService.createChat(createChatDto.participants);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get()
       async getChats(@Request() req): Promise<Chat[]> {
         return this.chatService.getChats(req.user.userId);
       }
    
       @UseGuards(JwtAuthGuard)
       @Get(':chatId')
       async getChatDetails(@Param('chatId') chatId: string): Promise<Chat> {
         return this.chatService.getChatDetails(chatId);
       }
    
       @UseGuards(JwtAuthGuard)
       @Post(':chatId/messages')
       async sendMessage(@Param('chatId') chatId: string, @Request() req, @Body() sendMessageDto: SendMessageDto): Promise<Chat> {
         return this.chatService.sendMessage(chatId, req.user.userId, sendMessageDto);
       }
     }
    
  5. Ensure JWT Auth Guard is Applied:

    • Ensure that JwtAuthGuard is properly set up and imported as shown in previous examples.

Frontend Code (Next.js)

  1. API Call for Sending Messages:

    • Create API Function for Sending Messages (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const sendMessage = async (token, chatId, content) => {
       try {
         const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    
  2. Chat Details Component:

    • Update Chat Details Component to Include Sending Messages (src/components/ChatDetails.js):
     import { useEffect, useState } from 'react';
     import { getChatDetails, sendMessage } from '../services/api';
    
     export default function ChatDetails({ chatId }) {
       const [chat, setChat] = useState(null);
       const [messageContent, setMessageContent] = useState('');
       const [error, setError] = useState('');
    
       useEffect(() => {
         const fetchChatDetails = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getChatDetails(token, chatId);
               setChat(data);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchChatDetails();
       }, [chatId]);
    
       const handleSendMessage = async (e) => {
         e.preventDefault();
         try {
           const token = localStorage.getItem('token');
           if (token) {
             const data = await sendMessage(token, chatId, messageContent);
             setChat(data);
             setMessageContent('');
           } else {
             setError('No token found');
           }
         } catch (err) {
           setError(err.message);
         }
       };
    
       if (error) {
         return <div className="text-red-500">{error}</div>;
       }
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat Details</h2>
           {chat ? (
             <div>
               <h3 className="text-xl
    

font-bold mb-3">Participants

    {chat.participants.map((participant) => (
  • {participant.username}
  • ))}

Messages


    {chat.messages.map((message) => (
  • {message.sender.username}: {message.content} {new Date(message.timestamp).toLocaleString()}
  • ))}


type="text"
value={messageContent}
onChange={(e) => setMessageContent(e.target.value)}
className="w-full px-3 py-2 border rounded mb-2"
placeholder="Type your message..."
/>
Send


) : (

Loading...


)}

);
}
```
  1. Usage in a Page:

    • Create a Page to Display Chat Details (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatDetails from '../../components/ChatDetails';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatDetails chatId={chatId} />;
     }
    

Conclusion

This setup includes the backend implementation for sending messages in a specific chat session with NestJS and a simple frontend chat details component with Next.js. The chat details component now includes functionality for sending messages. The chat details and message sending endpoints are protected with JWT authentication, ensuring only authenticated users can fetch and send messages in their chat sessions.

Messaging: Real-Time Message Receiving Using WebSocket

Backend Code (NestJS)

  1. WebSocket Gateway Setup:

    • Install WebSocket Dependencies:
     npm install @nestjs/websockets @nestjs/platform-socket.io
    
  2. WebSocket Gateway:

    • Create WebSocket Gateway (src/chat/chat.gateway.ts):
     import {
       SubscribeMessage,
       WebSocketGateway,
       WebSocketServer,
       OnGatewayInit,
       OnGatewayConnection,
       OnGatewayDisconnect,
     } from '@nestjs/websockets';
     import { Server, Socket } from 'socket.io';
     import { ChatService } from './chat.service';
     import { SendMessageDto } from './dto/send-message.dto';
    
     @WebSocketGateway()
     export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
       @WebSocketServer() server: Server;
    
       constructor(private readonly chatService: ChatService) {}
    
       afterInit(server: Server) {
         console.log('Init');
       }
    
       handleConnection(client: Socket, ...args: any[]) {
         console.log(`Client connected: ${client.id}`);
       }
    
       handleDisconnect(client: Socket) {
         console.log(`Client disconnected: ${client.id}`);
       }
    
       @SubscribeMessage('sendMessage')
       async handleMessage(client: Socket, payload: { chatId: string; userId: string; content: string }) {
         const message: SendMessageDto = { content: payload.content };
         const chat = await this.chatService.sendMessage(payload.chatId, payload.userId, message);
         this.server.to(payload.chatId).emit('receiveMessage', chat);
       }
    
       @SubscribeMessage('joinChat')
       handleJoinChat(client: Socket, chatId: string) {
         client.join(chatId);
         console.log(`Client ${client.id} joined chat ${chatId}`);
       }
     }
    
  3. Chat Service (Update for WebSocket):

    • Update Chat Service to Handle WebSocket Messages (src/chat/chat.service.ts):
     import { Injectable, NotFoundException } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { User, UserDocument } from '../schemas/user.schema';
     import { SendMessageDto } from './dto/send-message.dto';
    
     @Injectable()
     export class ChatService {
       constructor(
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
         @InjectModel(User.name) private userModel: Model<UserDocument>,
       ) {}
    
       async createChat(participants: string[]): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: participants,
           messages: [],
         });
         return newChat.save();
       }
    
       async getChats(userId: string): Promise<Chat[]> {
         return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec();
       }
    
       async getChatDetails(chatId: string): Promise<Chat> {
         return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec();
       }
    
       async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> {
         const chat = await this.chatModel.findById(chatId);
         if (!chat) {
           throw new NotFoundException('Chat not found');
         }
    
         chat.messages.push({
           sender: userId,
           content: sendMessageDto.content,
           timestamp: new Date(),
         });
    
         return chat.save();
       }
     }
    
  4. Chat Module:

    • Update Chat Module to Include WebSocket Gateway (src/chat/chat.module.ts):
     import { Module } from '@nestjs/common';
     import { MongooseModule } from '@nestjs/mongoose';
     import { ChatService } from './chat.service';
     import { ChatController } from './chat.controller';
     import { ChatGateway } from './chat.gateway';
     import { Chat, ChatSchema } from '../schemas/chat.schema';
     import { User, UserSchema } from '../schemas/user.schema';
    
     @Module({
       imports: [
         MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]),
         MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
       ],
       providers: [ChatService, ChatGateway],
       controllers: [ChatController],
     })
     export class ChatModule {}
    

Frontend Code (Next.js)

  1. WebSocket Client Setup:

    • Install Socket.io Client:
     npm install socket.io-client
    
  2. WebSocket Client:

    • Create WebSocket Client Hook (src/hooks/useChat.js):
     import { useEffect, useState } from 'react';
     import io from 'socket.io-client';
    
     const useChat = (chatId) => {
       const [messages, setMessages] = useState([]);
       const [message, setMessage] = useState('');
       const [socket, setSocket] = useState(null);
    
       useEffect(() => {
         const newSocket = io('http://localhost:3000');
         setSocket(newSocket);
    
         newSocket.emit('joinChat', chatId);
    
         newSocket.on('receiveMessage', (newChat) => {
           setMessages(newChat.messages);
         });
    
         return () => newSocket.close();
       }, [chatId]);
    
       const sendMessage = (userId, content) => {
         socket.emit('sendMessage', { chatId, userId, content });
         setMessage('');
       };
    
       return {
         messages,
         message,
         setMessage,
         sendMessage,
       };
     };
    
     export default useChat;
    
  3. Chat Details Component:

    • Update Chat Details Component to Include WebSocket (src/components/ChatDetails.js):
     import { useRouter } from 'next/router';
     import useChat from '../hooks/useChat';
    
     export default function ChatDetails({ chatId, userId }) {
       const router = useRouter();
       const { messages, message, setMessage, sendMessage } = useChat(chatId);
    
       const handleSendMessage = (e) => {
         e.preventDefault();
         sendMessage(userId, message);
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat Details</h2>
           <div>
             <h3 className="text-xl font-bold mb-3">Messages</h3>
             <ul>
               {messages.map((msg, index) => (
                 <li key={index} className="mb-2 p-2 border rounded">
                   <strong>{msg.sender.username}</strong>: {msg.content} <br />
                   <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span>
                 </li>
               ))}
             </ul>
             <form onSubmit={handleSendMessage} className="mt-4">
               <input
                 type="text"
                 value={message}
                 onChange={(e) => setMessage(e.target.value)}
                 className="w-full px-3 py-2 border rounded mb-2"
                 placeholder="Type your message..."
               />
               <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button>
             </form>
           </div>
         </div>
       );
     }
    
  4. Usage in a Page:

    • Update Page to Include User ID (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatDetails from '../../components/ChatDetails';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
       const userId = 'USER_ID'; // Replace with the actual user ID from authentication
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatDetails chatId={chatId} userId={userId} />;
     }
    

Conclusion

This setup includes the backend implementation for real-time message receiving using WebSocket with NestJS and a simple frontend implementation with Next.js using Socket.io. The ChatGateway handles the WebSocket events, and the frontend uses a custom hook to manage WebSocket connections and message state. This allows for real-time messaging capabilities in your chat application.

Real-time Communication: WebSocket Setup

Backend Code (NestJS)

  1. WebSocket Gateway Setup:

    • Install WebSocket Dependencies:
     npm install @nestjs/websockets @nestjs/platform-socket.io
    
  2. WebSocket Gateway:

    • Create WebSocket Gateway (src/chat/chat.gateway.ts):
     import {
       SubscribeMessage,
       WebSocketGateway,
       WebSocketServer,
       OnGatewayInit,
       OnGatewayConnection,
       OnGatewayDisconnect,
     } from '@nestjs/websockets';
     import { Server, Socket } from 'socket.io';
     import { ChatService } from './chat.service';
     import { SendMessageDto } from './dto/send-message.dto';
    
     @WebSocketGateway()
     export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
       @WebSocketServer() server: Server;
    
       constructor(private readonly chatService: ChatService) {}
    
       afterInit(server: Server) {
         console.log('WebSocket server initialized');
       }
    
       handleConnection(client: Socket) {
         console.log(`Client connected: ${client.id}`);
       }
    
       handleDisconnect(client: Socket) {
         console.log(`Client disconnected: ${client.id}`);
       }
    
       @SubscribeMessage('sendMessage')
       async handleMessage(client: Socket, payload: { chatId: string; userId: string; content: string }) {
         const message: SendMessageDto = { content: payload.content };
         const chat = await this.chatService.sendMessage(payload.chatId, payload.userId, message);
         this.server.to(payload.chatId).emit('receiveMessage', chat);
       }
    
       @SubscribeMessage('joinChat')
       handleJoinChat(client: Socket, chatId: string) {
         client.join(chatId);
         console.log(`Client ${client.id} joined chat ${chatId}`);
       }
     }
    
  3. Chat Service Update:

    • Update Chat Service to Handle WebSocket Messages (src/chat/chat.service.ts):
     import { Injectable, NotFoundException } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { User, UserDocument } from '../schemas/user.schema';
     import { SendMessageDto } from './dto/send-message.dto';
    
     @Injectable()
     export class ChatService {
       constructor(
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
         @InjectModel(User.name) private userModel: Model<UserDocument>,
       ) {}
    
       async createChat(participants: string[]): Promise<Chat> {
         const newChat = new this.chatModel({
           participants: participants,
           messages: [],
         });
         return newChat.save();
       }
    
       async getChats(userId: string): Promise<Chat[]> {
         return this.chatModel.find({ participants: userId }).populate('participants', 'username').exec();
       }
    
       async getChatDetails(chatId: string): Promise<Chat> {
         return this.chatModel.findById(chatId).populate('participants', 'username').populate('messages.sender', 'username').exec();
       }
    
       async sendMessage(chatId: string, userId: string, sendMessageDto: SendMessageDto): Promise<Chat> {
         const chat = await this.chatModel.findById(chatId);
         if (!chat) {
           throw new NotFoundException('Chat not found');
         }
    
         chat.messages.push({
           sender: userId,
           content: sendMessageDto.content,
           timestamp: new Date(),
         });
    
         return chat.save();
       }
     }
    
  4. Chat Module Update:

    • Update Chat Module to Include WebSocket Gateway (src/chat/chat.module.ts):
     import { Module } from '@nestjs/common';
     import { MongooseModule } from '@nestjs/mongoose';
     import { ChatService } from './chat.service';
     import { ChatController } from './chat.controller';
     import { ChatGateway } from './chat.gateway';
     import { Chat, ChatSchema } from '../schemas/chat.schema';
     import { User, UserSchema } from '../schemas/user.schema';
    
     @Module({
       imports: [
         MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]),
         MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
       ],
       providers: [ChatService, ChatGateway],
       controllers: [ChatController],
     })
     export class ChatModule {}
    

Frontend Code (Next.js)

  1. WebSocket Client Setup:

    • Install Socket.io Client:
     npm install socket.io-client
    
  2. WebSocket Client Hook:

    • Create WebSocket Client Hook (src/hooks/useChat.js):
     import { useEffect, useState } from 'react';
     import io from 'socket.io-client';
    
     const useChat = (chatId) => {
       const [messages, setMessages] = useState([]);
       const [message, setMessage] = useState('');
       const [socket, setSocket] = useState(null);
    
       useEffect(() => {
         const newSocket = io('http://localhost:3000');
         setSocket(newSocket);
    
         newSocket.emit('joinChat', chatId);
    
         newSocket.on('receiveMessage', (newChat) => {
           setMessages(newChat.messages);
         });
    
         return () => newSocket.close();
       }, [chatId]);
    
       const sendMessage = (userId, content) => {
         socket.emit('sendMessage', { chatId, userId, content });
         setMessage('');
       };
    
       return {
         messages,
         message,
         setMessage,
         sendMessage,
       };
     };
    
     export default useChat;
    
  3. Chat Details Component:

    • Update Chat Details Component to Include WebSocket (src/components/ChatDetails.js):
     import { useEffect, useState } from 'react';
     import useChat from '../hooks/useChat';
    
     export default function ChatDetails({ chatId, userId }) {
       const { messages, message, setMessage, sendMessage } = useChat(chatId);
    
       const handleSendMessage = (e) => {
         e.preventDefault();
         sendMessage(userId, message);
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat Details</h2>
           <div>
             <h3 className="text-xl font-bold mb-3">Messages</h3>
             <ul>
               {messages.map((msg, index) => (
                 <li key={index} className="mb-2 p-2 border rounded">
                   <strong>{msg.sender.username}</strong>: {msg.content} <br />
                   <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span>
                 </li>
               ))}
             </ul>
             <form onSubmit={handleSendMessage} className="mt-4">
               <input
                 type="text"
                 value={message}
                 onChange={(e) => setMessage(e.target.value)}
                 className="w-full px-3 py-2 border rounded mb-2"
                 placeholder="Type your message..."
               />
               <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button>
             </form>
           </div>
         </div>
       );
     }
    
  4. Usage in a Page:

    • Update Page to Include User ID (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatDetails from '../../components/ChatDetails';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
       const userId = 'USER_ID'; // Replace with the actual user ID from authentication
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatDetails chatId={chatId} userId={userId} />;
     }
    

Conclusion

This setup includes the backend implementation for real-time message receiving and sending using WebSocket with NestJS and a simple frontend implementation with Next.js using Socket.io. The ChatGateway handles the WebSocket events, and the frontend uses a custom hook to manage WebSocket connections and message state. This allows for real-time messaging capabilities in your chat application, ensuring that messages are sent and received in real time.

User Interface: Login Page

Frontend Code (Next.js)

  1. Create Login Page:

    • Create Login Page Component (src/pages/login.js):
     import { useState } from 'react';
     import { useRouter } from 'next/router';
     import { login } from '../services/api';
    
     export default function LoginPage() {
       const [username, setUsername] = useState('');
       const [password, setPassword] = useState('');
       const [error, setError] = useState('');
       const router = useRouter();
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           const data = await login(username, password);
           localStorage.setItem('token', data.access_token);
           router.push('/chats');
         } catch (err) {
           setError(err.message);
         }
       };
    
       return (
         <div className="flex items-center justify-center h-screen bg-gray-100">
           <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg">
             <h2 className="text-2xl font-bold text-center">Login</h2>
             <form className="space-y-6" onSubmit={handleSubmit}>
               <div>
                 <label className="block text-gray-700">Username</label>
                 <input
                   type="text"
                   value={username}
                   onChange={(e) => setUsername(e.target.value)}
                   className="w-full px-3 py-2 border rounded"
                   required
                 />
               </div>
               <div>
                 <label className="block text-gray-700">Password</label>
                 <input
                   type="password"
                   value={password}
                   onChange={(e) => setPassword(e.target.value)}
                   className="w-full px-3 py-2 border rounded"
                   required
                 />
               </div>
               <div>
                 <button
                   type="submit"
                   className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700"
                 >
                   Login
                 </button>
               </div>
               {error && <p className="text-red-500">{error}</p>}
             </form>
           </div>
         </div>
       );
     }
    
  2. API Call for Login:

    • Update API Function for Login (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const sendMessage = async (token, chatId, content) => {
       try {
         const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    

Conclusion

This setup includes the frontend implementation of a login page using Next.js and Tailwind CSS. The login page consists of a form with input fields for the username and password, and a submit button to log in. The login API call is handled using an async function that communicates with the backend. Upon successful login, the user is redirected to the chat page. Any error during login is displayed to the user.

User Interface: Registration Page

Frontend Code (Next.js)

  1. Create Registration Page:

    • Create Registration Page Component (src/pages/register.js):
     import { useState } from 'react';
     import { useRouter } from 'next/router';
     import { register } from '../services/api';
    
     export default function RegisterPage() {
       const [username, setUsername] = useState('');
       const [password, setPassword] = useState('');
       const [error, setError] = useState('');
       const [success, setSuccess] = useState('');
       const router = useRouter();
    
       const handleSubmit = async (e) => {
         e.preventDefault();
         try {
           await register(username, password);
           setSuccess('Registration successful! You can now login.');
           setUsername('');
           setPassword('');
           setError('');
           setTimeout(() => {
             router.push('/login');
           }, 2000);
         } catch (err) {
           setError(err.message);
           setSuccess('');
         }
       };
    
       return (
         <div className="flex items-center justify-center h-screen bg-gray-100">
           <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg">
             <h2 className="text-2xl font-bold text-center">Register</h2>
             <form className="space-y-6" onSubmit={handleSubmit}>
               <div>
                 <label className="block text-gray-700">Username</label>
                 <input
                   type="text"
                   value={username}
                   onChange={(e) => setUsername(e.target.value)}
                   className="w-full px-3 py-2 border rounded"
                   required
                 />
               </div>
               <div>
                 <label className="block text-gray-700">Password</label>
                 <input
                   type="password"
                   value={password}
                   onChange={(e) => setPassword(e.target.value)}
                   className="w-full px-3 py-2 border rounded"
                   required
                 />
               </div>
               <div>
                 <button
                   type="submit"
                   className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700"
                 >
                   Register
                 </button>
               </div>
               {error && <p className="text-red-500">{error}</p>}
               {success && <p className="text-green-500">{success}</p>}
             </form>
           </div>
         </div>
       );
     }
    
  2. API Call for Registration:

    • Update API Function for Registration (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const sendMessage = async (token, chatId, content) => {
       try {
         const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    

Conclusion

This setup includes the frontend implementation of a registration page using Next.js and Tailwind CSS. The registration page consists of a form with input fields for the username and password, and a submit button to register. The registration API call is handled using an async function that communicates with the backend. Upon successful registration, the user is notified and redirected to the login page. Any error during registration is displayed to the user.

User Interface: Chat List Page and Chat Window

Frontend Code (Next.js)

Chat List Page

  1. Create Chat List Page:

    • Create Chat List Page Component (src/pages/chats.js):
     import { useEffect, useState } from 'react';
     import { useRouter } from 'next/router';
     import { getChats } from '../services/api';
    
     export default function ChatListPage() {
       const [chats, setChats] = useState([]);
       const [search, setSearch] = useState('');
       const [error, setError] = useState('');
       const router = useRouter();
    
       useEffect(() => {
         const fetchChats = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getChats(token);
               setChats(data);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchChats();
       }, []);
    
       const filteredChats = chats.filter(chat =>
         chat.participants.some(participant =>
           participant.username.toLowerCase().includes(search.toLowerCase())
         )
       );
    
       const handleChatClick = (chatId) => {
         router.push(`/chat/${chatId}`);
       };
    
       if (error) {
         return <div className="text-red-500">{error}</div>;
       }
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chats</h2>
           <input
             type="text"
             value={search}
             onChange={(e) => setSearch(e.target.value)}
             className="w-full px-3 py-2 border rounded mb-5"
             placeholder="Search chats..."
           />
           {filteredChats.length > 0 ? (
             <ul>
               {filteredChats.map(chat => (
                 <li
                   key={chat._id}
                   className="mb-2 p-2 border rounded cursor-pointer"
                   onClick={() => handleChatClick(chat._id)}
                 >
                   {chat.participants.map(participant => participant.username).join(', ')}
                 </li>
               ))}
             </ul>
           ) : (
             <p>No chats available.</p>
           )}
         </div>
       );
     }
    

Chat Window

  1. Create Chat Window Component:

    • Create Chat Window Component (src/components/ChatWindow.js):
     import { useEffect, useState } from 'react';
     import useChat from '../hooks/useChat';
    
     export default function ChatWindow({ chatId, userId }) {
       const { messages, message, setMessage, sendMessage } = useChat(chatId);
    
       const handleSendMessage = (e) => {
         e.preventDefault();
         sendMessage(userId, message);
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat</h2>
           <div>
             <ul className="mb-5">
               {messages.map((msg, index) => (
                 <li key={index} className="mb-2 p-2 border rounded">
                   <strong>{msg.sender.username}</strong>: {msg.content} <br />
                   <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span>
                 </li>
               ))}
             </ul>
             <form onSubmit={handleSendMessage}>
               <input
                 type="text"
                 value={message}
                 onChange={(e) => setMessage(e.target.value)}
                 className="w-full px-3 py-2 border rounded mb-2"
                 placeholder="Type your message..."
               />
               <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                 Send
               </button>
             </form>
           </div>
         </div>
       );
     }
    
  2. WebSocket Client Hook (from previous steps):

    • Ensure you have the useChat hook (src/hooks/useChat.js):
     import { useEffect, useState } from 'react';
     import io from 'socket.io-client';
    
     const useChat = (chatId) => {
       const [messages, setMessages] = useState([]);
       const [message, setMessage] = useState('');
       const [socket, setSocket] = useState(null);
    
       useEffect(() => {
         const newSocket = io('http://localhost:3000');
         setSocket(newSocket);
    
         newSocket.emit('joinChat', chatId);
    
         newSocket.on('receiveMessage', (newChat) => {
           setMessages(newChat.messages);
         });
    
         return () => newSocket.close();
       }, [chatId]);
    
       const sendMessage = (userId, content) => {
         socket.emit('sendMessage', { chatId, userId, content });
         setMessage('');
       };
    
       return {
         messages,
         message,
         setMessage,
         sendMessage,
       };
     };
    
     export default useChat;
    
  3. Usage in a Page:

    • Create Page for Chat Window (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatWindow from '../../components/ChatWindow';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
       const userId = 'USER_ID'; // Replace with the actual user ID from authentication
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatWindow chatId={chatId} userId={userId} />;
     }
    

Conclusion

This setup includes the frontend implementation for the chat list page and chat window using Next.js and Tailwind CSS. The chat list page displays the list of chat sessions with a search bar, allowing users to filter chats. The chat window displays chat messages and includes an input field and send button for sending new messages. The useChat hook manages the WebSocket connection and real-time message handling.

Notifications: Real-time Notifications for New Messages

Frontend Code (Next.js)

  1. WebSocket Client Hook Enhancement:

    • Update useChat Hook to Handle Notifications (src/hooks/useChat.js):
     import { useEffect, useState } from 'react';
     import io from 'socket.io-client';
    
     const useChat = (chatId, onNewMessage) => {
       const [messages, setMessages] = useState([]);
       const [message, setMessage] = useState('');
       const [socket, setSocket] = useState(null);
    
       useEffect(() => {
         const newSocket = io('http://localhost:3000');
         setSocket(newSocket);
    
         newSocket.emit('joinChat', chatId);
    
         newSocket.on('receiveMessage', (newChat) => {
           setMessages(newChat.messages);
           if (onNewMessage) {
             const newMessage = newChat.messages[newChat.messages.length - 1];
             onNewMessage(newMessage);
           }
         });
    
         return () => newSocket.close();
       }, [chatId]);
    
       const sendMessage = (userId, content) => {
         socket.emit('sendMessage', { chatId, userId, content });
         setMessage('');
       };
    
       return {
         messages,
         message,
         setMessage,
         sendMessage,
       };
     };
    
     export default useChat;
    
  2. Notification Component:

    • Create Notification Component (src/components/Notification.js):
     export default function Notification({ message }) {
       if (!message) return null;
    
       return (
         <div className="fixed bottom-0 right-0 mb-4 mr-4 p-4 bg-blue-500 text-white rounded shadow-lg">
           <strong>{message.sender.username}</strong>: {message.content}
         </div>
       );
     }
    
  3. Chat Details Component with Notifications:

    • Update Chat Details Component to Show Notifications (src/components/ChatDetails.js):
     import { useState } from 'react';
     import useChat from '../hooks/useChat';
     import Notification from './Notification';
    
     export default function ChatDetails({ chatId, userId }) {
       const [notification, setNotification] = useState(null);
       const { messages, message, setMessage, sendMessage } = useChat(chatId, setNotification);
    
       const handleSendMessage = (e) => {
         e.preventDefault();
         sendMessage(userId, message);
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat</h2>
           <div>
             <ul className="mb-5">
               {messages.map((msg, index) => (
                 <li key={index} className="mb-2 p-2 border rounded">
                   <strong>{msg.sender.username}</strong>: {msg.content} <br />
                   <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span>
                 </li>
               ))}
             </ul>
             <form onSubmit={handleSendMessage}>
               <input
                 type="text"
                 value={message}
                 onChange={(e) => setMessage(e.target.value)}
                 className="w-full px-3 py-2 border rounded mb-2"
                 placeholder="Type your message..."
               />
               <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                 Send
               </button>
             </form>
           </div>
           <Notification message={notification} />
         </div>
       );
     }
    
  4. Usage in a Page:

    • Ensure Page Includes User ID (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatDetails from '../../components/ChatDetails';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
       const userId = 'USER_ID'; // Replace with the actual user ID from authentication
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatDetails chatId={chatId} userId={userId} />;
     }
    

Conclusion

This setup includes the frontend implementation for real-time notifications for new messages using Next.js. The useChat hook is enhanced to notify the component of new messages. A Notification component displays the notification at the bottom right corner of the screen. The ChatDetails component is updated to show notifications whenever a new message is received. This ensures that users are promptly notified of new messages in real-time.

File Sharing: Upload and Download Files

Backend Code (NestJS)

  1. Install Required Dependencies:

    • Install Multer for File Uploads:
     npm install @nestjs/platform-express multer
    
  2. File Schema:

    • Create File Schema (src/schemas/file.schema.ts):
     import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
     import { Document, Types } from 'mongoose';
    
     export type FileDocument = File & Document;
    
     @Schema()
     export class File {
       @Prop({ required: true })
       filename: string;
    
       @Prop({ required: true })
       path: string;
    
       @Prop({ required: true })
       mimetype: string;
    
       @Prop({ type: Types.ObjectId, ref: 'Chat' })
       chat: Types.ObjectId;
     }
    
     export const FileSchema = SchemaFactory.createForClass(File);
    
  3. Chat Schema Update:

    • Update Chat Schema to Include Files (src/schemas/chat.schema.ts):
     import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
     import { Document, Types } from 'mongoose';
    
     export type ChatDocument = Chat & Document;
    
     @Schema()
     export class Chat {
       @Prop({ type: [{ type: Types.ObjectId, ref: 'User' }], required: true })
       participants: Types.ObjectId[];
    
       @Prop({ type: [{ sender: { type: Types.ObjectId, ref: 'User' }, content: String, timestamp: Date }], default: [] })
       messages: { sender: Types.ObjectId; content: string; timestamp: Date }[];
    
       @Prop({ type: [{ type: Types.ObjectId, ref: 'File' }], default: [] })
       files: Types.ObjectId[];
     }
    
     export const ChatSchema = SchemaFactory.createForClass(Chat);
    
  4. File Service:

    • Create File Service to Handle File Upload and Download (src/file/file.service.ts):
     import { Injectable, NotFoundException } from '@nestjs/common';
     import { InjectModel } from '@nestjs/mongoose';
     import { Model } from 'mongoose';
     import { Chat, ChatDocument } from '../schemas/chat.schema';
     import { File, FileDocument } from '../schemas/file.schema';
     import { Express } from 'express';
     import { join } from 'path';
    
     @Injectable()
     export class FileService {
       constructor(
         @InjectModel(File.name) private fileModel: Model<FileDocument>,
         @InjectModel(Chat.name) private chatModel: Model<ChatDocument>,
       ) {}
    
       async uploadFile(chatId: string, file: Express.Multer.File): Promise<File> {
         const chat = await this.chatModel.findById(chatId);
         if (!chat) {
           throw new NotFoundException('Chat not found');
         }
    
         const newFile = new this.fileModel({
           filename: file.filename,
           path: file.path,
           mimetype: file.mimetype,
           chat: chatId,
         });
    
         chat.files.push(newFile._id);
         await chat.save();
         return newFile.save();
       }
    
       async downloadFile(fileId: string): Promise<File> {
         const file = await this.fileModel.findById(fileId);
         if (!file) {
           throw new NotFoundException('File not found');
         }
         return file;
       }
    
       getFilePath(file: File): string {
         return join(__dirname, '..', '..', file.path);
       }
     }
    
  5. File Controller:

    • Create File Controller to Handle File Upload and Download (src/file/file.controller.ts):
     import { Controller, Post, Get, Param, UploadedFile, UseInterceptors, Res, NotFoundException } from '@nestjs/common';
     import { FileInterceptor } from '@nestjs/platform-express';
     import { FileService } from './file.service';
     import { diskStorage } from 'multer';
     import { v4 as uuidv4 } from 'uuid';
     import { Express, Response } from 'express';
    
     @Controller('chats/:chatId/files')
     export class FileController {
       constructor(private readonly fileService: FileService) {}
    
       @Post()
       @UseInterceptors(
         FileInterceptor('file', {
           storage: diskStorage({
             destination: './uploads',
             filename: (req, file, cb) => {
               const filename = `${uuidv4()}-${file.originalname}`;
               cb(null, filename);
             },
           }),
         }),
       )
       async uploadFile(
         @Param('chatId') chatId: string,
         @UploadedFile() file: Express.Multer.File,
       ) {
         return this.fileService.uploadFile(chatId, file);
       }
    
       @Get(':fileId')
       async downloadFile(
         @Param('fileId') fileId: string,
         @Res() res: Response,
       ) {
         const file = await this.fileService.downloadFile(fileId);
         if (!file) {
           throw new NotFoundException('File not found');
         }
         res.sendFile(this.fileService.getFilePath(file));
       }
     }
    
  6. File Module:

    • Create File Module (src/file/file.module.ts):
     import { Module } from '@nestjs/common';
     import { MongooseModule } from '@nestjs/mongoose';
     import { FileService } from './file.service';
     import { FileController } from './file.controller';
     import { File, FileSchema } from '../schemas/file.schema';
     import { Chat, ChatSchema } from '../schemas/chat.schema';
    
     @Module({
       imports: [
         MongooseModule.forFeature([{ name: File.name, schema: FileSchema }]),
         MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]),
       ],
       providers: [FileService],
       controllers: [FileController],
     })
     export class FileModule {}
    
  7. App Module Update:

    • Update App Module to Include File Module (src/app.module.ts):
     import { Module } from '@nestjs/common';
     import { MongooseModule } from '@nestjs/mongoose';
     import { AuthModule } from './auth/auth.module';
     import { ChatModule } from './chat/chat.module';
     import { FileModule } from './file/file.module';
    
     @Module({
       imports: [
         MongooseModule.forRoot('mongodb://localhost/chat-app'),
         AuthModule,
         ChatModule,
         FileModule,
       ],
     })
     export class AppModule {}
    

Frontend Code (Next.js)

  1. API Call for File Upload and Download:

    • Create API Functions for File Upload and Download (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const sendMessage = async (token, chatId, content) => {
       try {
         const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export
    

const uploadFile = async (token, chatId, file) => {
try {
const formData = new FormData();
formData.append('file', file);

     const response = await axios.post(`${API_URL}/chats/${chatId}/files`, formData, {
       headers: {
         Authorization: `Bearer ${token}`,
         'Content-Type': 'multipart/form-data',
       },
     });
     return response.data;
   } catch (error) {
     throw error.response.data;
   }
 };

 export const downloadFile = async (token, chatId, fileId) => {
   try {
     const response = await axios.get(`${API_URL}/chats/${chatId}/files/${fileId}`, {
       headers: {
         Authorization: `Bearer ${token}`,
       },
       responseType: 'blob',
     });
     return response.data;
   } catch (error) {
     throw error.response.data;
   }
 };

 export const logout = () => {
   localStorage.removeItem('token');
   window.location.href = '/login'; // Redirect to login page
 };
 ```
Enter fullscreen mode Exit fullscreen mode
  1. File Upload Component:

    • Create File Upload Component (src/components/FileUpload.js):
     import { useState } from 'react';
     import { uploadFile } from '../services/api';
    
     export default function FileUpload({ chatId }) {
       const [file, setFile] = useState(null);
       const [message, setMessage] = useState('');
    
       const handleFileChange = (e) => {
         setFile(e.target.files[0]);
       };
    
       const handleUpload = async (e) => {
         e.preventDefault();
         try {
           const token = localStorage.getItem('token');
           await uploadFile(token, chatId, file);
           setMessage('File uploaded successfully');
           setFile(null);
         } catch (err) {
           setMessage(`Error: ${err.message}`);
         }
       };
    
       return (
         <div className="mb-4">
           <form onSubmit={handleUpload}>
             <input type="file" onChange={handleFileChange} className="mb-2" />
             <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
               Upload File
             </button>
           </form>
           {message && <p className="mt-2">{message}</p>}
         </div>
       );
     }
    
  2. File Download Component:

    • Create File Download Component (src/components/FileDownload.js):
     import { downloadFile } from '../services/api';
    
     export default function FileDownload({ chatId, file }) {
       const handleDownload = async () => {
         try {
           const token = localStorage.getItem('token');
           const fileData = await downloadFile(token, chatId, file._id);
    
           const url = window.URL.createObjectURL(new Blob([fileData]));
           const link = document.createElement('a');
           link.href = url;
           link.setAttribute('download', file.filename);
           document.body.appendChild(link);
           link.click();
         } catch (err) {
           console.error('Error downloading file:', err);
         }
       };
    
       return (
         <div className="mb-2">
           <button onClick={handleDownload} className="bg-green-500 text-white px-4 py-2 rounded">
             Download {file.filename}
           </button>
         </div>
       );
     }
    
  3. Chat Window with File Sharing:

    • Update Chat Window Component to Include File Upload and Download (src/components/ChatWindow.js):
     import { useState } from 'react';
     import useChat from '../hooks/useChat';
     import FileUpload from './FileUpload';
     import FileDownload from './FileDownload';
    
     export default function ChatWindow({ chatId, userId }) {
       const { messages, message, setMessage, sendMessage } = useChat(chatId);
       const [files, setFiles] = useState([]);
    
       const handleSendMessage = (e) => {
         e.preventDefault();
         sendMessage(userId, message);
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">Chat</h2>
           <div>
             <ul className="mb-5">
               {messages.map((msg, index) => (
                 <li key={index} className="mb-2 p-2 border rounded">
                   <strong>{msg.sender.username}</strong>: {msg.content} <br />
                   <span className="text-gray-500 text-sm">{new Date(msg.timestamp).toLocaleString()}</span>
                 </li>
               ))}
             </ul>
             <form onSubmit={handleSendMessage}>
               <input
                 type="text"
                 value={message}
                 onChange={(e) => setMessage(e.target.value)}
                 className="w-full px-3 py-2 border rounded mb-2"
                 placeholder="Type your message..."
               />
               <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
                 Send
               </button>
             </form>
             <FileUpload chatId={chatId} />
             <div className="mt-4">
               {files.map(file => (
                 <FileDownload key={file._id} chatId={chatId} file={file} />
               ))}
             </div>
           </div>
         </div>
       );
     }
    
  4. Usage in a Page:

    • Update Page to Include File Sharing (src/pages/chat/[chatId].js):
     import { useRouter } from 'next/router';
     import ChatWindow from '../../components/ChatWindow';
    
     export default function ChatPage() {
       const router = useRouter();
       const { chatId } = router.query;
       const userId = 'USER_ID'; // Replace with the actual user ID from authentication
    
       if (!chatId) {
         return <div>Loading...</div>;
       }
    
       return <ChatWindow chatId={chatId} userId={userId} />;
     }
    

Conclusion

This setup includes the backend implementation for uploading and downloading files in a chat session with NestJS, and the frontend implementation with Next.js. Users can upload files to a chat session, and download them from the chat session. The FileUpload component handles file uploads, and the FileDownload component handles file downloads. The ChatWindow component is updated to include file upload and download functionalities, ensuring comprehensive file sharing capabilities in your chat application.

Settings: User Settings Page

Frontend Code (Next.js)

  1. Create User Settings Page:

    • Create User Settings Page Component (src/pages/settings.js):
     import { useState, useEffect } from 'react';
     import { getProfile, updateProfile } from '../services/api';
    
     export default function SettingsPage() {
       const [username, setUsername] = useState('');
       const [notificationPreference, setNotificationPreference] = useState(true);
       const [message, setMessage] = useState('');
       const [error, setError] = useState('');
    
       useEffect(() => {
         const fetchProfile = async () => {
           try {
             const token = localStorage.getItem('token');
             if (token) {
               const data = await getProfile(token);
               setUsername(data.username);
               setNotificationPreference(data.notificationPreference || true);
             } else {
               setError('No token found');
             }
           } catch (err) {
             setError(err.message);
           }
         };
    
         fetchProfile();
       }, []);
    
       const handleSave = async (e) => {
         e.preventDefault();
         try {
           const token = localStorage.getItem('token');
           const updateData = { username, notificationPreference };
           await updateProfile(token, updateData);
           setMessage('Settings updated successfully');
         } catch (err) {
           setError(err.message);
         }
       };
    
       return (
         <div className="max-w-md mx-auto mt-10">
           <h2 className="text-2xl font-bold mb-5">User Settings</h2>
           {error && <p className="text-red-500">{error}</p>}
           {message && <p className="text-green-500">{message}</p>}
           <form onSubmit={handleSave} className="space-y-4">
             <div>
               <label className="block text-gray-700">Username</label>
               <input
                 type="text"
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 className="w-full px-3 py-2 border rounded"
                 required
               />
             </div>
             <div>
               <label className="block text-gray-700">Notification Preferences</label>
               <select
                 value={notificationPreference}
                 onChange={(e) => setNotificationPreference(e.target.value === 'true')}
                 className="w-full px-3 py-2 border rounded"
               >
                 <option value="true">Enable Notifications</option>
                 <option value="false">Disable Notifications</option>
               </select>
             </div>
             <div>
               <button
                 type="submit"
                 className="w-full px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700"
               >
                 Save Settings
               </button>
             </div>
           </form>
         </div>
       );
     }
    
  2. API Call for Updating Profile:

    • Update API Function for Updating Profile (src/services/api.js):
     import axios from 'axios';
    
     const API_URL = 'http://localhost:3000';
    
     export const register = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/register`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const login = async (username, password) => {
       try {
         const response = await axios.post(`${API_URL}/auth/login`, { username, password });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getProfile = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/auth/me`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const updateProfile = async (token, userData) => {
       try {
         const response = await axios.put(`${API_URL}/auth/me`, userData, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const createChat = async (token, participants) => {
       try {
         const response = await axios.post(`${API_URL}/chats`, { participants }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChats = async (token) => {
       try {
         const response = await axios.get(`${API_URL}/chats`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const getChatDetails = async (token, chatId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const sendMessage = async (token, chatId, content) => {
       try {
         const response = await axios.post(`${API_URL}/chats/${chatId}/messages`, { content }, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const uploadFile = async (token, chatId, file) => {
       try {
         const formData = new FormData();
         formData.append('file', file);
    
         const response = await axios.post(`${API_URL}/chats/${chatId}/files`, formData, {
           headers: {
             Authorization: `Bearer ${token}`,
             'Content-Type': 'multipart/form-data',
           },
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const downloadFile = async (token, chatId, fileId) => {
       try {
         const response = await axios.get(`${API_URL}/chats/${chatId}/files/${fileId}`, {
           headers: {
             Authorization: `Bearer ${token}`,
           },
           responseType: 'blob',
         });
         return response.data;
       } catch (error) {
         throw error.response.data;
       }
     };
    
     export const logout = () => {
       localStorage.removeItem('token');
       window.location.href = '/login'; // Redirect to login page
     };
    

Conclusion

This setup includes the frontend implementation for a user settings page using Next.js and Tailwind CSS. The user settings page allows users to update their username and notification preferences. The settings are fetched from and updated to the backend using appropriate API calls. This ensures that users can manage their account settings and preferences effectively.

Future Enhancements

1. Voice and Video Calls

Description: Adding support for voice and video calls.

Implementation Plan:

  1. WebRTC Integration:

    • Use WebRTC for real-time communication.
    • Set up signaling server to manage WebRTC connections.
  2. Signaling Server:

    • Implement signaling server using Socket.io or any other preferred WebSocket library.
    • Handle offer, answer, and ICE candidate exchange.
  3. Frontend UI:

    • Add buttons for voice and video calls.
    • Create components for managing WebRTC streams (e.g., <VideoCall />, <VoiceCall />).
  4. Backend API:

    • Add endpoints for initiating and ending calls.
    • Store call history and status.

Frontend Code Example:

// src/components/VideoCall.js
import React, { useRef, useEffect, useState } from 'react';
import io from 'socket.io-client';

const VideoCall = ({ userId, chatId }) => {
  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(null);
  const localVideoRef = useRef();
  const remoteVideoRef = useRef();
  const socket = useRef(null);
  const peerConnection = useRef(null);

  useEffect(() => {
    socket.current = io('http://localhost:3000');

    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then(stream => {
        setLocalStream(stream);
        localVideoRef.current.srcObject = stream;
      });

    socket.current.on('offer', handleOffer);
    socket.current.on('answer', handleAnswer);
    socket.current.on('ice-candidate', handleICECandidate);

    return () => {
      socket.current.disconnect();
      if (peerConnection.current) {
        peerConnection.current.close();
      }
    };
  }, []);

  const handleOffer = async (offer) => {
    peerConnection.current = createPeerConnection();
    await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer));
    const answer = await peerConnection.current.createAnswer();
    await peerConnection.current.setLocalDescription(answer);
    socket.current.emit('answer', { answer, chatId });
  };

  const handleAnswer = async (answer) => {
    await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer));
  };

  const handleICECandidate = (candidate) => {
    if (peerConnection.current) {
      peerConnection.current.addIceCandidate(new RTCIceCandidate(candidate));
    }
  };

  const createPeerConnection = () => {
    const pc = new RTCPeerConnection();
    pc.onicecandidate = (event) => {
      if (event.candidate) {
        socket.current.emit('ice-candidate', { candidate: event.candidate, chatId });
      }
    };
    pc.ontrack = (event) => {
      setRemoteStream(event.streams[0]);
      remoteVideoRef.current.srcObject = event.streams[0];
    };
    localStream.getTracks().forEach(track => {
      pc.addTrack(track, localStream);
    });
    return pc;
  };

  const startCall = async () => {
    peerConnection.current = createPeerConnection();
    const offer = await peerConnection.current.createOffer();
    await peerConnection.current.setLocalDescription(offer);
    socket.current.emit('offer', { offer, chatId });
  };

  return (
    <div>
      <video ref={localVideoRef} autoPlay muted />
      <video ref={remoteVideoRef} autoPlay />
      <button onClick={startCall}>Start Call</button>
    </div>
  );
};

export default VideoCall;
Enter fullscreen mode Exit fullscreen mode

2. Group Chats

Description: Adding support for group chat functionality.

Implementation Plan:

  1. Database Schema:

    • Update chat schema to include group chat properties (e.g., group name, members).
    • Modify existing chat schema to distinguish between individual and group chats.
  2. Backend API:

    • Create endpoints for creating, updating, and deleting group chats.
    • Handle adding and removing members from group chats.
  3. Frontend UI:

    • Add UI for creating and managing group chats.
    • Update chat list and chat window components to support group chats.

Backend Code Example:

// src/chat/chat.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Chat, ChatDocument } from '../schemas/chat.schema';
import { CreateGroupChatDto } from './dto/create-group-chat.dto';
import { AddMembersDto } from './dto/add-members.dto';

@Injectable()
export class ChatService {
  constructor(@InjectModel(Chat.name) private chatModel: Model<ChatDocument>) {}

  async createGroupChat(createGroupChatDto: CreateGroupChatDto): Promise<Chat> {
    const newGroupChat = new this.chatModel({
      ...createGroupChatDto,
      isGroupChat: true,
      messages: [],
    });
    return newGroupChat.save();
  }

  async addMembers(chatId: string, addMembersDto: AddMembersDto): Promise<Chat> {
    const chat = await this.chatModel.findById(chatId);
    if (!chat || !chat.isGroupChat) {
      throw new NotFoundException('Group chat not found');
    }
    chat.members.push(...addMembersDto.members);
    return chat.save();
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Status Indicators

Description: Adding online/offline status indicators for users.

Implementation Plan:

  1. Backend WebSocket Integration:

    • Track user connection and disconnection events.
    • Store user status in a database or in-memory store.
  2. Frontend UI:

    • Add UI elements to display user status (e.g., green dot for online, gray dot for offline).
    • Update user list and chat components to show status indicators.

Backend Code Example:

// src/chat/chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { UserService } from '../user/user.service';

@WebSocketGateway()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server;

  constructor(private readonly userService: UserService) {}

  afterInit(server: Server) {
    console.log('WebSocket server initialized');
  }

  async handleConnection(client: Socket) {
    const userId = client.handshake.query.userId;
    await this.userService.updateUserStatus(userId, true);
    this.server.emit('userStatus', { userId, status: 'online' });
  }

  async handleDisconnect(client: Socket) {
    const userId = client.handshake.query.userId;
    await this.userService.updateUserStatus(userId, false);
    this.server.emit('userStatus', { userId, status: 'offline' });
  }
}
Enter fullscreen mode Exit fullscreen mode

Frontend Code Example:

// src/components/UserList.js
import { useEffect, useState } from 'react';
import io from 'socket.io-client';

export default function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const socket = io('http://localhost:3000');

    socket.on('userStatus', (data) => {
      setUsers((prevUsers) =>
        prevUsers.map((user) =>
          user._id === data.userId ? { ...user, status: data.status } : user
        )
      );
    });

    return () => socket.disconnect();
  }, []);

  return (
    <div>
      <h2 className="text-2xl font-bold mb-5">Users</h2>
      <ul>
        {users.map((user) => (
          <li key={user._id} className="flex items-center mb-2">
            <span
              className={`w-2.5 h-2.5 rounded-full ${
                user.status === 'online' ? 'bg-green-500' : 'bg-gray-500'
              }`}
            ></span>
            <span className="ml-2">{user.username}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

These enhancements outline the future features of the chat application, including voice and video calls, group chat functionality, and user status indicators. Each enhancement involves both backend and frontend changes to ensure seamless integration and improved user experience.

Implementing Google Meet or Zoom-like Meeting Feature with Sharable Link

To implement a Google Meet or Zoom-like meeting feature with a sharable link, we can use WebRTC for real-time video and audio communication. Additionally, we can use Socket.io for signaling and managing the connections. Here's a high-level overview and implementation guide:

Backend Code (NestJS)

  1. WebSocket Gateway Setup
  • Install WebSocket Dependencies:
  npm install @nestjs/websockets @nestjs/platform-socket.io
Enter fullscreen mode Exit fullscreen mode
  • Create WebSocket Gateway:

    • Create WebSocket Gateway (src/meeting/meeting.gateway.ts):
    import {
      SubscribeMessage,
      WebSocketGateway,
      WebSocketServer,
      OnGatewayInit,
      OnGatewayConnection,
      OnGatewayDisconnect,
    } from '@nestjs/websockets';
    import { Server, Socket } from 'socket.io';
    
    @WebSocketGateway()
    export class MeetingGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
      @WebSocketServer() server: Server;
    
      afterInit(server: Server) {
        console.log('WebSocket server initialized');
      }
    
      handleConnection(client: Socket) {
        console.log(`Client connected: ${client.id}`);
      }
    
      handleDisconnect(client: Socket) {
        console.log(`Client disconnected: ${client.id}`);
      }
    
      @SubscribeMessage('joinMeeting')
      handleJoinMeeting(client: Socket, meetingId: string) {
        client.join(meetingId);
        client.broadcast.to(meetingId).emit('userJoined', client.id);
      }
    
      @SubscribeMessage('signal')
      handleSignal(client: Socket, payload: { meetingId: string; signal: any }) {
        client.broadcast.to(payload.meetingId).emit('signal', {
          userId: client.id,
          signal: payload.signal,
        });
      }
    }
    
  1. Meeting Module:

    • Create Meeting Module (src/meeting/meeting.module.ts):
    import { Module } from '@nestjs/common';
    import { MeetingGateway } from './meeting.gateway';
    
    @Module({
      providers: [MeetingGateway],
    })
    export class MeetingModule {}
    
  2. Update App Module:

    • Update App Module to Include Meeting Module (src/app.module.ts):
    import { Module } from '@nestjs/common';
    import { MongooseModule } from '@nestjs/mongoose';
    import { AuthModule } from './auth/auth.module';
    import { ChatModule } from './chat/chat.module';
    import { FileModule } from './file/file.module';
    import { MeetingModule } from './meeting/meeting.module';
    
    @Module({
      imports: [
        MongooseModule.forRoot('mongodb://localhost/chat-app'),
        AuthModule,
        ChatModule,
        FileModule,
        MeetingModule,
      ],
    })
    export class AppModule {}
    

Frontend Code (Next.js)

  1. Install Socket.io Client:
  npm install socket.io-client
Enter fullscreen mode Exit fullscreen mode
  1. WebRTC Setup:

    • Create WebRTC Hook (src/hooks/useWebRTC.js):
    import { useEffect, useRef, useState } from 'react';
    import io from 'socket.io-client';
    
    const useWebRTC = (meetingId) => {
      const [remoteStreams, setRemoteStreams] = useState([]);
      const localStream = useRef(null);
      const socket = useRef(null);
      const peerConnections = useRef({});
    
      useEffect(() => {
        socket.current = io('http://localhost:3000');
    
        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
          .then(stream => {
            localStream.current.srcObject = stream;
    
            socket.current.emit('joinMeeting', meetingId);
    
            socket.current.on('userJoined', userId => {
              const peerConnection = createPeerConnection(userId);
              peerConnections.current[userId] = peerConnection;
              stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
            });
    
            socket.current.on('signal', async ({ userId, signal }) => {
              if (peerConnections.current[userId]) {
                await peerConnections.current[userId].setRemoteDescription(new RTCSessionDescription(signal));
                if (signal.type === 'offer') {
                  const answer = await peerConnections.current[userId].createAnswer();
                  await peerConnections.current[userId].setLocalDescription(answer);
                  socket.current.emit('signal', { meetingId, signal: answer });
                }
              } else {
                const peerConnection = createPeerConnection(userId);
                await peerConnection.setRemoteDescription(new RTCSessionDescription(signal));
                peerConnections.current[userId] = peerConnection;
                if (signal.type === 'offer') {
                  const answer = await peerConnection.createAnswer();
                  await peerConnection.setLocalDescription(answer);
                  socket.current.emit('signal', { meetingId, signal: answer });
                }
              }
            });
          });
    
        return () => {
          Object.values(peerConnections.current).forEach(pc => pc.close());
          socket.current.disconnect();
        };
      }, [meetingId]);
    
      const createPeerConnection = (userId) => {
        const pc = new RTCPeerConnection();
        pc.onicecandidate = (event) => {
          if (event.candidate) {
            socket.current.emit('signal', { meetingId, signal: event.candidate });
          }
        };
        pc.ontrack = (event) => {
          setRemoteStreams((prevStreams) => {
            const existingStream = prevStreams.find(stream => stream.id === event.streams[0].id);
            if (existingStream) return prevStreams;
            return [...prevStreams, event.streams[0]];
          });
        };
        return pc;
      };
    
      return { localStream, remoteStreams };
    };
    
    export default useWebRTC;
    
  2. Meeting Component:

    • Create Meeting Component (src/components/Meeting.js):
    import React, { useRef } from 'react';
    import useWebRTC from '../hooks/useWebRTC';
    
    const Meeting = ({ meetingId }) => {
      const { localStream, remoteStreams } = useWebRTC(meetingId);
      const localVideoRef = useRef();
    
      useEffect(() => {
        if (localStream.current) {
          localVideoRef.current.srcObject = localStream.current.srcObject;
        }
      }, [localStream]);
    
      return (
        <div>
          <video ref={localVideoRef} autoPlay muted className="local-video" />
          <div className="remote-videos">
            {remoteStreams.map((stream, index) => (
              <video key={index} autoPlay className="remote-video" ref={video => {
                if (video) {
                  video.srcObject = stream;
                }
              }} />
            ))}
          </div>
        </div>
      );
    };
    
    export default Meeting;
    
  3. Usage in a Page:

    • Create Meeting Page (src/pages/meeting/[meetingId].js):
    import { useRouter } from 'next/router';
    import Meeting from '../../components/Meeting';
    
    export default function MeetingPage() {
      const router = useRouter();
      const { meetingId } = router.query;
    
      if (!meetingId) {
        return <div>Loading...</div>;
      }
    
      return <Meeting meetingId={meetingId} />;
    }
    

Conclusion

This setup provides a basic implementation of a Google Meet or Zoom-like meeting feature with a sharable link using WebRTC for real-time communication and Socket.io for signaling. The backend uses NestJS to manage WebSocket connections, and the frontend uses Next.js to handle the video call UI and WebRTC interactions. This implementation can be further extended with additional features such as screen sharing, chat, and more.

Adding Screen Sharing Feature

To add a screen sharing feature, we can use the getDisplayMedia method from the WebRTC API. This allows users to share their screen during the video call.

Frontend Code (Next.js)

  1. Update WebRTC Hook:

    • Enhance the useWebRTC hook to include screen sharing functionality (src/hooks/useWebRTC.js):
     import { useEffect, useRef, useState } from 'react';
     import io from 'socket.io-client';
    
     const useWebRTC = (meetingId) => {
       const [remoteStreams, setRemoteStreams] = useState([]);
       const localStream = useRef(null);
       const screenStream = useRef(null);
       const socket = useRef(null);
       const peerConnections = useRef({});
    
       useEffect(() => {
         socket.current = io('http://localhost:3000');
    
         navigator.mediaDevices.getUserMedia({ video: true, audio: true })
           .then(stream => {
             localStream.current = stream;
             attachStreamToVideo(localStream.current, 'local-video');
    
             socket.current.emit('joinMeeting', meetingId);
    
             socket.current.on('userJoined', userId => {
               const peerConnection = createPeerConnection(userId);
               peerConnections.current[userId] = peerConnection;
               stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
             });
    
             socket.current.on('signal', async ({ userId, signal }) => {
               if (peerConnections.current[userId]) {
                 await peerConnections.current[userId].setRemoteDescription(new RTCSessionDescription(signal));
                 if (signal.type === 'offer') {
                   const answer = await peerConnections.current[userId].createAnswer();
                   await peerConnections.current[userId].setLocalDescription(answer);
                   socket.current.emit('signal', { meetingId, signal: answer });
                 }
               } else {
                 const peerConnection = createPeerConnection(userId);
                 await peerConnection.setRemoteDescription(new RTCSessionDescription(signal));
                 peerConnections.current[userId] = peerConnection;
                 if (signal.type === 'offer') {
                   const answer = await peerConnection.createAnswer();
                   await peerConnection.setLocalDescription(answer);
                   socket.current.emit('signal', { meetingId, signal: answer });
                 }
               }
             });
           });
    
         return () => {
           Object.values(peerConnections.current).forEach(pc => pc.close());
           socket.current.disconnect();
         };
       }, [meetingId]);
    
       const createPeerConnection = (userId) => {
         const pc = new RTCPeerConnection();
         pc.onicecandidate = (event) => {
           if (event.candidate) {
             socket.current.emit('signal', { meetingId, signal: event.candidate });
           }
         };
         pc.ontrack = (event) => {
           setRemoteStreams((prevStreams) => {
             const existingStream = prevStreams.find(stream => stream.id === event.streams[0].id);
             if (existingStream) return prevStreams;
             return [...prevStreams, event.streams[0]];
           });
         };
         return pc;
       };
    
       const attachStreamToVideo = (stream, videoId) => {
         const videoElement = document.getElementById(videoId);
         if (videoElement) {
           videoElement.srcObject = stream;
         }
       };
    
       const startScreenSharing = async () => {
         try {
           screenStream.current = await navigator.mediaDevices.getDisplayMedia({ video: true });
           const screenTrack = screenStream.current.getVideoTracks()[0];
    
           Object.values(peerConnections.current).forEach(pc => {
             const sender = pc.getSenders().find(s => s.track.kind === 'video');
             if (sender) {
               sender.replaceTrack(screenTrack);
             }
           });
    
           screenTrack.onended = () => {
             stopScreenSharing();
           };
         } catch (error) {
           console.error('Error starting screen sharing:', error);
         }
       };
    
       const stopScreenSharing = () => {
         const videoTrack = localStream.current.getVideoTracks()[0];
         Object.values(peerConnections.current).forEach(pc => {
           const sender = pc.getSenders().find(s => s.track.kind === 'video');
           if (sender) {
             sender.replaceTrack(videoTrack);
           }
         });
         screenStream.current.getTracks().forEach(track => track.stop());
         screenStream.current = null;
       };
    
       return { localStream, remoteStreams, startScreenSharing, stopScreenSharing };
     };
    
     export default useWebRTC;
    
  2. Update Meeting Component:

    • Update the Meeting component to include screen sharing buttons (src/components/Meeting.js):
     import React, { useRef, useEffect } from 'react';
     import useWebRTC from '../hooks/useWebRTC';
    
     const Meeting = ({ meetingId }) => {
       const { localStream, remoteStreams, startScreenSharing, stopScreenSharing } = useWebRTC(meetingId);
       const localVideoRef = useRef();
    
       useEffect(() => {
         if (localStream.current) {
           localVideoRef.current.srcObject = localStream.current.srcObject;
         }
       }, [localStream]);
    
       return (
         <div>
           <video ref={localVideoRef} autoPlay muted id="local-video" className="local-video" />
           <div className="remote-videos">
             {remoteStreams.map((stream, index) => (
               <video key={index} autoPlay className="remote-video" ref={video => {
                 if (video) {
                   video.srcObject = stream;
                 }
               }} />
             ))}
           </div>
           <button onClick={startScreenSharing} className="bg-blue-500 text-white px-4 py-2 rounded">Share Screen</button>
           <button onClick={stopScreenSharing} className="bg-red-500 text-white px-4 py-2 rounded">Stop Sharing</button>
         </div>
       );
     };
    
     export default Meeting;
    
  3. Usage in a Page:

    • Ensure the meeting page uses the updated Meeting component (src/pages/meeting/[meetingId].js):
     import { useRouter } from 'next/router';
     import Meeting from '../../components/Meeting';
    
     export default function MeetingPage() {
       const router = useRouter();
       const { meetingId } = router.query;
    
       if (!meetingId) {
         return <div>Loading...</div>;
       }
    
       return <Meeting meetingId={meetingId} />;
     }
    

Backend Code (NestJS)

  1. No Change Required for Backend:
    • The existing WebSocket gateway setup remains the same, as the signaling process for screen sharing is handled similarly to regular video/audio streams. The signal event will carry the necessary signaling data for screen sharing as well.

Conclusion

This setup includes the frontend implementation for a screen sharing feature using WebRTC and Next.js. The useWebRTC hook is enhanced to manage screen sharing, allowing users to start and stop screen sharing during a video call. The Meeting component includes buttons to control screen sharing. This feature enhances the overall meeting experience by enabling users to share their screens seamlessly.

Disclaimer: This content is generated by AI.

Top comments (0)