Nest JS Websockets - Basics
Tags: react, websocket, typescript, showdev
Published: 2022-12-05 04:36:12

Welcome to part 1, the **Basics** of building a chat application with [Nest JS](https://docs.nestjs.com/) and [React](https://reactjs.org/).

This series will attempt to touch on as many features of Nest's websockets integration as well as socket.io in general.

The entire series will be building on one repository that has both the server and the client for the application, so for each part you can download the repository from specific commits to follow along or you can clone the entire repository in it's finished state.

Download the repository at **[this commit](https://github.com/ahoward2/nest-react-websockets/tree/ae9d1f7c4e3dcfb9075ba52eaece21e1ea967ecd)** to follow along.

## Background

**Nest JS** is a framework that provides a level of abstraction above common Node frameworks like [Express](https://expressjs.com/), [Fastify](https://www.fastify.io/), and importantly for this use case - [Socket.io](https://socket.io/). Nest is particularly opinionated about architecture which on one hand means you're "pigeon holed", but on the other hand they've made decisions for you which can allow you to focus on building your application.

**React** doesn't need much of an introduction as it's such a popular framework. Nevertheless React is a framework for building interactive user interfaces with a ton of broad support across the internet. Given any roadblocks you might run into, someone has most likely posted a solution somewhere or is likely to help you out.

**Socket.io** is a Node framework around websockets which allow for two-way communication between clients (our React client) and servers (our Nest server) - otherwise known as [full-duplex](<https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX>) systems.

## Design

For a basic first iteration of the application, we'll shoot for this kind of overall design:

- We want our clients to be able to **emit** messages to our server via a form input.
- We want our server to **subscribe** to messages coming in from clients, and **emit** or **broadcast** them out to all connected clients.

![basic architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cftgchzcue9agfmtr15g.png)

## File Structure

For this project we're developing both the **client** and the **server** in the same repository. The Nest server is also serving the React client so it makes since to have them together.

The core structure at this stage looks like this:

- **public**: static files like images and importantly _index.html_ which gets served initially on page render.
- **src**
  - **client**: react client source files.
  - **server**: nest server source files.
  - **shared**: source files shared by client and server - mostly share Typescript types for full-stack type safety.
- **...configuration and miscellaneous files**: the rest of the files in the root directory are files such as _.gitignore_, _package.json_, _webpack.config.client.js_ for bundling our react client, _tailwind.config.js_ for our css library configuration, etc.

## The Server

At this stage our server is quite small - only 4 files.

- **chat**
  - **chat.gateway.ts**
  - **chat.module.ts**
- **app.module.ts**
- **main.ts**

Let's take a look at each of the files one by one and see what's happening in them.

```ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  await app.listen(3000);

  // Gracefully shutdown the server.
  app.enableShutdownHooks();
}
bootstrap();
```

The **main.ts** file is our server's entrypoint. We the server boots up, this is the first file that's executed. As was mentioned before, Nest provides a level of abstraction above Express and Fastify - and we are going with the Exress flavor here as seen when we create the server with `NestExpressApplication` from the `'@nestjs/platform-express'` package. Our server runs on port `3000` and we make use of a handy `app.enableShutdownHooks()` to shut down our server gracefully in the event that the running process is killed - [read about shutdown hooks here](https://docs.nestjs.com/fundamentals/lifecycle-events#application-shutdown).

```ts
import { Module } from '@nestjs/common';
import { ChatModule } from './chat/chat.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

@Module({
  imports: [
    ChatModule,
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', '..', '..', 'dist', 'client'),
    }),
  ],
})
export class AppModule {}
```

Nest employs the concept of [modules](https://docs.nestjs.com/modules) as part of it's core architecture. Our **app.module.ts** file contains our _root_ module which is the starting point that Nest uses to build the **application graph**. Any other modules in the server would be children of the application module and should be imported into their direct parents. In this case, we have a **ChatModule** that we'll take a look at shortly, but here we are adding it to our application module's `imports: []` array.

We want our Nest server to also serve our client React application. To do that we can make use of the `ServeStaticModule` from `'@nestjs/serve-static'`. It takes a `rootPath` path to the bundle of our client that gets produced by webpack.

```ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import {
  ServerToClientEvents,
  ClientToServerEvents,
  Message,
} from '../../shared/interfaces/chat.interface';
import { Server } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class ChatGateway {
  @WebSocketServer() server: Server = new Server<
    ServerToClientEvents,
    ClientToServerEvents
  >();
  private logger = new Logger('ChatGateway');

  @SubscribeMessage('chat')
  async handleEvent(
    @MessageBody()
    payload: Message,
  ): Promise<Message> {
    this.logger.log(payload);
    this.server.emit('chat', payload); // broadcast messages
    return payload;
  }
}
```

Here is the really interesting part of the server. This file is the websocket gateway for our chat application - see [Gateways](https://docs.nestjs.com/websockets/gateways). All we need to do to create a gateway is importing and using the `WebsocketGateway` class and pass it some configuration options - in our case we're setting [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to be wide open to all origins by using `'*'`. This is not considered safe and should only be open to trusted origins - we will address this later in future series parts.

Nest supports 2 websocket platforms - [socket.io](https://socket.io/) and [ws](https://github.com/websockets/ws). We're going to be using socket.io.

To get access to the underlying socket.io server, we can use the `WebSocketServer` class to instantiate a `server` with a type of socket.io `Server`. One extremely cool thing we can do here in terms of Typescript is passing in event types `ServerToClientEvents` and `ClientToServerEvents`. We will touch on these again when we go over our **shared** directory.

We want to log chat messages coming into the server from clients so we also instantiate a `logger` with Nest's `Logger` class.

We also need to subscribe to incoming messages from clients. To do this we can use the `SubscribeMessage` class which takes the name of the messages `chat`. This requires passing a function to handle the incoming message which takes a `MessageBody` payload of type `Message` which we also get from our **shared** directory since both client and server will want to know the message type. We then log the message with `this.logger.log(payload)` and then emit the message by invoking the socket.io server's `emit` method with `this.server.emit('chat', payload)`. This will broadcast the message to all clients that have subscribed to `chat` messages. Then we return the payload.

```ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [ChatGateway],
})
export class ChatModule {}
```

The **chat.module.ts** file is as simple as it gets. Nest treats `WebSocketGateway`s as [providers](https://docs.nestjs.com/providers) which like [modules](https://docs.nestjs.com/modules) are part of it's core architecture. We pass our `ChatGateway` into `providers: []`.

## The Client

We're going to try to focus on the websockets aspect of the React client and skip over some things like component styling, webpack configuration, postcss configuration, etc. One thing you'll also notice is we don't have any client-side routing - which we may end up adding in later series parts. But for now we're sticking to the basics.

Our server directory looks like this:

- **components**: UI components.
- **layouts**: UI page layouts.
- **styles**: core css style sheet.
- **app.tsx**: core application file.
- **index.tsx**: client application entry point.

Let's just focus on the `app.tsx` file which contains the core logic of the frontend.

```tsx
import React, { useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import {
  User,
  Message,
  ServerToClientEvents,
  ClientToServerEvents,
} from '../shared/interfaces/chat.interface';
import { Header } from './components/header';
import { LoginForm } from './components/login.form';
import { MessageForm } from './components/message.form';
import { Messages } from './components/messages';
import { ChatLayout } from './layouts/chat.layout';
import { LoginLayout } from './layouts/login.layout';

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

function App() {
  const [isConnected, setIsConnected] = useState(socket.connected);
  const [messages, setMessages] = useState<Message[]>([]);
  const [user, setUser] = useState<User>();

  useEffect(() => {
    const currentUser = JSON.parse(sessionStorage.getItem('user') ?? '{}');
    if (currentUser.userId) {
      setUser(currentUser);
    }

    socket.on('connect', () => {
      setIsConnected(true);
    });

    socket.on('disconnect', () => {
      setIsConnected(false);
    });

    socket.on('chat', (e) => {
      setMessages((messages) => [e, ...messages]);
    });

    return () => {
      socket.off('connect');
      socket.off('disconnect');
      socket.off('chat');
    };
  }, []);

  const login = (e: React.FormEvent<HTMLFormElement>) => {
    const formValue = e.target[0].value;
    const newUser = {
      userId: Date.now().toLocaleString().concat(formValue),
      userName: formValue,
    };
    sessionStorage.setItem('user', JSON.stringify(newUser));
    setUser(newUser);
  };

  const sendMessage = (e: React.FormEvent<HTMLFormElement>) => {
    if (user) {
      socket.emit('chat', {
        user: {
          userId: user.userId,
          userName: user.userName,
        },
        timeSent: new Date(Date.now()).toLocaleString('en-US'),
        message: e.target[0].value,
      });
    }
  };
  return (
    <>
      {user && user.userId ? (
        <ChatLayout>
          <Header user={user} isConnected={isConnected}></Header>
          <Messages user={user} messages={messages}></Messages>
          <MessageForm sendMessage={sendMessage}></MessageForm>
        </ChatLayout>
      ) : (
        <LoginLayout>
          <LoginForm login={login}></LoginForm>
        </LoginLayout>
      )}
    </>
  );
}

export default App;
```

Since we're using socket.io for our server websocket platform, we're also going to use another package that they ship called `'socket.io-client'`.

After we import all the things we need at the top of the file, the first important thing we do is create a new [socket](https://socket.io/docs/v4/client-api/#socket) instance with the `io()` function that we import from the package.

Like in our chat gatway on the server, we're passing in our shared types `ServerToClientEvents` and `ClientToServerEvents`.

We have 3 [`useState`](https://reactjs.org/docs/hooks-state.html) hooks where we're storing some state in the client.

```tsx
// We want to store the client's current state of the connection.
const [isConnected, setIsConnected] = useState(socket.connected)
// We want to store a list of messages that have been emitted from the server.
const [messages, setMessages] = useState<Message[]>([])
// We want to store the current user of the client.
const [user, setUser] = useState<User>()
```

We also have one [`useEffect`](https://reactjs.org/docs/hooks-effect.html) hook where we want to perform some side effects. We're going to leverage a browser api [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) to temporarily store the currently "logged in" user in the browser. There are some nuances between sessionStorage and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). For this version of the application we're going to use sessionStorage so that we can log in a new user per each browser tab that we open for easy demonstration purposes. We're also not doing any authentication at the moment so we need not burden ourselves with security at this stage - sessionStorage is just fine for now.

```tsx
useEffect(() => {
  // Grab the currently "logged in" user from sessionStorage
  const currentUser = JSON.parse(sessionStorage.getItem('user') ?? '{}')
  // If userId exists, we know there is a "logged in" user.
  if (currentUser.userId) {
    // So we set the current user into state.
    setUser(currentUser)
  }

  // We go ahead and setup an event listener for a 'connect' event.
  socket.on('connect', () => {
    // Set connect state accordingly.
    setIsConnected(true)
  })

  // We go ahead and setup an event listener for a 'disconnect' event.
  socket.on('disconnect', () => {
    // Set connect state accordingly.
    setIsConnected(false)
  })

  // We go ahead and setup an event listener for a 'disconnect' event.
  socket.on('chat', (e) => {
    // Set connect state accordingly.
    setMessages((messages) => [e, ...messages])
  })

  // We define a cleanup procedure to remove event listeners.
  return () => {
    socket.off('connect')
    socket.off('disconnect')
    socket.off('chat')
  }
}, [])
```

We define a `login` function that we can execute when a new user submits their name in a login form.

```tsx
const login = (e: React.FormEvent<HTMLFormElement>) => {
  // Extract user input from the form.
  const formValue = e.target[0].value
  // Construct a new user object with user input formValue.
  const newUser = {
    // Unique userId by concating current date and the user's name.
    // The edge case here would be if two users of the same name logged
    // in at the exact same millisecond in time.
    // We can solve this edge case later with authentication.
    // Ex) "1,668,125,065,218anna"
    userId: Date.now().toLocaleString().concat(formValue),
    userName: formValue,
  }
  // Set new user to session storage.
  sessionStorage.setItem('user', JSON.stringify(newUser))
  // Set new user to state.
  setUser(newUser)
}
```

We define another function `sendMessage` that we can execute when a user submits a message in a message form.

```tsx
const sendMessage = (e: React.FormEvent<HTMLFormElement>) => {
  // There must be a user set in state to send a message.
  if (user) {
    // Given a user is "logged in", emit a 'chat' message to the server
    // with the current user's information, the time of the message (timeSent),
    // and the value of the user input in the message form.
    socket.emit('chat', {
      user: {
        userId: user.userId,
        userName: user.userName,
      },
      timeSent: new Date(Date.now()).toLocaleString('en-US'),
      message: e.target[0].value,
    })
  }
}
```

Here is a sample `Message` object:

```json
{
  "user": {
    "userId": "1,667,963,682,100anna",
    "userName": "anna"
  },
  "timeSent": "11/10/2022, 8:41:35 PM",
  "message": "🤨"
}
```

Finally we return some tsx/jsx elements from our `app` function. We won't look into each and every React component for the sake of brevity but it's encouraged to browse through the **components** and **layout** directory to see what's happening.

```tsx
return (
  <>
    {/* If a user is "logged in" (set in state), render chat elements. */}
    {user && user.userId ? (
      <ChatLayout>
        <Header user={user} isConnected={isConnected}></Header>
        <Messages user={user} messages={messages}></Messages>
        <MessageForm sendMessage={sendMessage}></MessageForm>
      </ChatLayout>
    ) : (
      {/* If a user is not "logged in" (set in state), render login elements. */}
      <LoginLayout>
        <LoginForm login={login}></LoginForm>
      </LoginLayout>
    )}
  </>
)
```

That's it for our client for now! Here's what we're left with:

_Login UI_

![login user interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lp17bb7ornr6u9nedh7c.png)

_Chat UI_

![chat user interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oar347rgxn1wegkoozn3.png)

## Shared Types

Before we wrap this up we should touch on one of the key features of this setup - shared types.

Checkout the [Typescript docs](https://socket.io/docs/v4/typescript/) on socket.io.

In our **shared** directory we have a common **chat.interface.ts** file.

```ts
export interface User {
  userId: string;
  userName: string;
}

export interface Message {
  user: User;
  timeSent: string;
  message: string;
}

// Interface for when server emits events to clients.
export interface ServerToClientEvents {
  chat: (e: Message) => void;
}

// Interface for when clients emit events to the server.
export interface ClientToServerEvents {
  chat: (e: Message) => void;
}
```

If you can remember before, we're using these event interfaces in both the client and the server.

```ts
// ...

@WebSocketServer() server: Server = new Server<
    ServerToClientEvents,
    ClientToServerEvents
  >();

// ...

async handleEvent(
  @MessageBody()
  payload: Message,
): Promise<Message> {
  this.logger.log(payload);
  this.server.emit('chat', payload); // <--- ServerToClientEvent
  return payload;
}

// ...
```

```tsx
// ...

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

// ...

socket.emit('chat', { // <--- ClientToServerEvent
  user: {
    userId: user.userId,
    userName: user.userName,
  },
  timeSent: new Date(Date.now()).toLocaleString('en-US'),
  message: e.target[0].value,
});

// ...
```

With these interfaces, we have end-to-end type safety which provides us with a strong layer of assurance that our server and clients will receive the correct data! Super cool.

---

That's all for this part of the series! Go check out the code and more specifically the UI components that we didn't touch on here like the `Messages`, `MessageForm`, and the `LoginForm` and take a look at some of the tailwind css that we've done for styling them.

Download the repository at **[this commit](https://github.com/ahoward2/nest-react-websockets/tree/ae9d1f7c4e3dcfb9075ba52eaece21e1ea967ecd)** to follow along.