Real-Time Applications with NestJS and WebSockets: From Zero to Hero

NestJS and WebSockets create dynamic real-time apps. NestJS offers structure and scalability, while WebSockets enable two-way communication. Together, they power interactive experiences like chat apps and live updates.

Real-Time Applications with NestJS and WebSockets: From Zero to Hero

Real-time applications are all the rage these days, and for good reason. They offer a level of interactivity and responsiveness that traditional web apps just can’t match. If you’re looking to dive into this exciting world, NestJS and WebSockets are your new best friends.

Let’s start with NestJS. It’s a progressive Node.js framework that’s been gaining traction lately, and it’s easy to see why. It combines the best of both worlds – the efficiency of JavaScript and the structure of TypeScript. Plus, it’s built with scalability in mind, making it perfect for enterprise-level applications.

Now, onto WebSockets. These bad boys allow for real-time, two-way communication between clients and servers. Unlike traditional HTTP requests, WebSockets maintain an open connection, allowing data to flow freely without the overhead of constantly establishing new connections.

When you combine NestJS and WebSockets, you’ve got a powerhouse duo for building real-time applications. Think chat apps, live sports updates, collaborative tools – the possibilities are endless.

So, how do we get started? First things first, let’s set up our NestJS project. If you haven’t already, you’ll need to install Node.js and npm. Once that’s done, open up your terminal and run:

npm i -g @nestjs/cli
nest new real-time-app
cd real-time-app

This will create a new NestJS project for us. Now, let’s add WebSocket support:

npm install --save @nestjs/websockets @nestjs/platform-socket.io

Great! We’re all set up. Now, let’s create a simple WebSocket gateway. In NestJS, gateways are similar to controllers, but they’re used for handling WebSocket events instead of HTTP requests.

Create a new file called chat.gateway.ts in your src folder:

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

@WebSocketGateway()
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(client: any, payload: string): void {
    this.server.emit('message', payload);
  }
}

This gateway listens for ‘message’ events and broadcasts them to all connected clients. Simple, right?

Now, let’s add this gateway to our app.module.ts:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatGateway } from './chat.gateway';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, ChatGateway],
})
export class AppModule {}

And that’s it for the server side! Now, let’s create a simple HTML file to test our WebSocket connection. Create a new file called index.html in your project root:

<!DOCTYPE html>
<html>
<head>
    <title>NestJS WebSocket Chat</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
    <script>
        const socket = io('http://localhost:3000');
        
        function sendMessage() {
            const messageInput = document.getElementById('messageInput');
            socket.emit('message', messageInput.value);
            messageInput.value = '';
        }

        socket.on('message', (message) => {
            const messagesDiv = document.getElementById('messages');
            messagesDiv.innerHTML += `<p>${message}</p>`;
        });
    </script>
</head>
<body>
    <div id="messages"></div>
    <input type="text" id="messageInput">
    <button onclick="sendMessage()">Send</button>
</body>
</html>

Now, start your NestJS server with npm run start:dev, open the HTML file in your browser, and voila! You’ve got a real-time chat application.

But wait, there’s more! This is just scratching the surface of what you can do with NestJS and WebSockets. Let’s explore some more advanced concepts.

One of the cool things about NestJS is its support for decorators. We’ve already seen the @WebSocketGateway() and @SubscribeMessage() decorators, but there are more. For example, you can use @ConnectedSocket() to get access to the socket instance:

@SubscribeMessage('message')
handleMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: string): void {
  // Do something with the client socket
}

You can also use middleware with your WebSocket gateways. This is great for things like authentication or logging. Here’s an example:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Socket } from 'socket.io';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(socket: Socket, next: (err?: any) => void) {
    const token = socket.handshake.auth.token;
    if (isValidToken(token)) {
      next();
    } else {
      next(new Error('Authentication error'));
    }
  }
}

To use this middleware, you’d add it to your gateway like this:

@WebSocketGateway()
@UseMiddleware(AuthMiddleware)
export class ChatGateway {
  // ...
}

Now, let’s talk about scaling. As your application grows, you might need to handle more and more concurrent connections. This is where Redis comes in handy. Redis can act as a pub/sub broker, allowing you to scale your WebSocket application across multiple servers.

First, install the required packages:

npm install @socket.io/redis-adapter redis

Then, update your main.ts file:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis-io.adapter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis();
  
  app.useWebSocketAdapter(redisIoAdapter);
  
  await app.listen(3000);
}
bootstrap();

And create a new file called redis-io.adapter.ts:

import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({ url: `redis://localhost:6379` });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

Now your WebSocket server can scale horizontally across multiple nodes, with Redis handling the pub/sub communication between them.

One last thing to consider is error handling. In a real-time application, things can go wrong in, well, real-time. It’s important to have robust error handling in place. NestJS provides an @UseFilters() decorator that we can use with WebSocket exceptions:

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter } from '@nestjs/websockets';

@Catch()
export class WebSocketExceptionFilter extends BaseWsExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
    const client = host.switchToWs().getClient();
    client.emit('error', { message: 'An error occurred' });
  }
}

Then, apply it to your gateway:

@WebSocketGateway()
@UseFilters(new WebSocketExceptionFilter())
export class ChatGateway {
  // ...
}

And there you have it! You’ve gone from zero to hero with real-time applications using NestJS and WebSockets. You’ve learned how to set up a basic WebSocket server, handle messages, use middleware for authentication, scale your application with Redis, and even handle errors gracefully.

Remember, the key to mastering real-time applications is practice. Start with simple projects and gradually increase complexity. Before you know it, you’ll be building the next big real-time app. Happy coding!