[Hono] Simple Messaging App Using Bun and WebSocket
Tags: hono, bunjs, typescript, react
Published: 2024-06-10 01:11:38

We often see implementations of WebSocket using the Express framework and Socket.io. However, there seem to be fewer examples of WebSocket implementations using Hono, a framework that is similar to Express but faster and lighter. In this article, I will introduce the implementation of a simple messaging app using Hono and Bun, a JavaScript runtime.

This project has a simple structure, and it is possible to extend various features such as database utilization and multiple room management functions. Initially, I aimed to write an article focused on WebSocket that could be read in less than 3 minutes, but I was drawn in by the charm of Hono, and the volume of the article increased more than expected. Now, let’s move on to the details.

## Tech Stack

Frontend:
- React
- TypeScript
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)

Backend:
- [Hono](https://hono.dev/)
- TypeScript

JavaScript Runtime(Both Frontend and Backend):
- [Bun](https://bun.sh/) v1.1.8

## Project Structure

```txt
├── frontend
│   ├── src
│   │   ├── App.css
│   │   ├── App.tsx
│   │   ├── index.css
│   │   ├── main.tsx
│   │   └── vite-env.d.ts
│   ├── bun.lockb
│   ├── index.html
│   ├── package.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── server
│   └── index.ts
├── shared
│   ├── constants.ts
│   └── types.ts
├── bun.lockb
├── package.json
└── tsconfig.json
```

- Root directory and server directory: Hono app
- Frontend directory: React app
- Shared directory: Constants and types used in common between frontend and backend

I have omitted settings files for TailwindCSS, etc. The structure is such that the Hono app wraps the React app. This was an easy-to-manage configuration when implementing RPC (Remote Procedure Call), where the frontend and backend share dependencies and type definitions of Hono.

Repository:
https://github.com/yutakusuno/bun-hono-react-websocket

## UI

![app demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r8bv2j7eulj6blduj1mv.gif)


## What is Hono?

Hono is a web application framework that is extremely fast and lightweight. It operates on any JavaScript runtime and includes built-in middleware and helpers. Its implementation is similar to Express, making it intuitive to use. It also features a clean API and first-class support for TypeScript.

For more details, visit: https://hono.dev/

## What is Bun?

Bun is a JavaScript runtime and an all-in-one toolkit for JavaScript and TypeScript applications. It is written in Zig and internally uses JavaScriptCore, a performance-oriented JS engine created for Safari. It also implements Node.js and Web API natively and provides all the tools necessary to build JavaScript applications, including a package manager, test runner, and bundler.

For more details, visit: https://bun.sh/

## What is WebSocket?

WebSocket is a protocol that creates a persistent bidirectional communication channel between a web browser and a server. With this technology, web applications can exchange data with the server in real-time without the client initiating a new HTTP request or reloading the page.

The operation of WebSocket goes through the following process:

**Starting the handshake**: The client sends the following HTTP request to the server, requesting an upgrade from HTTP to WebSocket.

```txt
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
```

**Server response**: If the server supports WebSocket and agrees to the upgrade, the server responds with its own handshake, confirming the switch to the WebSocket protocol.

```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
```

**Data transmission**: While maintaining an open connection, the client and server can exchange data.

![WebSocket Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtz76azbqfd2zwy19in8.png)

This diagram shows the basic flow of WebSocket communication between the client and server. First, the client requests an upgrade to WebSocket from the server via an HTTP request. Then, the server returns an HTTP response, confirming the protocol switch. This opens the connection, and the client and server can exchange data.

I would like to delve deeper into this area to understand it better, but that’s it for now.

The request and response examples for the handshake are quoted from the RFC.
https://datatracker.ietf.org/doc/html/rfc6455#section-1.2

## Implementation

I will focus on key implementations by extracting parts from this repository. I want to explain as much as possible, including the import of dependencies and the definition of variables, so there are parts where the actual implementation has been rewritten as needed. If you want to understand in detail, please read the source code of the repository in parallel, treating this article as supplementary.

You can check out the completed application here: https://github.com/yutakusuno/bun-hono-react-websocket

### Backend: WebSocket Configuration

server/index.ts

```typescript
import { Hono } from 'hono';
import { createBunWebSocket } from 'hono/bun';

const app = new Hono();
const { upgradeWebSocket, websocket } = createBunWebSocket();
const server = Bun.serve({
  fetch: app.fetch,
  port: 3000,
  websocket,
});

export default app;
```

When launching an HTTP server with Bun, it is recommended to use `Bun.serve`. Pass an instance of the Hono class to this fetch handler and specify the port of the backend server. The websocket imported from `createBunWebSocket` is a Hono middleware, WebSocket handler, implemented for Bun.

### Backend: WebSocket Connection and Disconnection

server/index.ts

```typescript
import type { ServerWebSocket } from 'bun';

const topic = 'anonymous-chat-room';

app.get(
  '/ws',
  upgradeWebSocket((_) => ({
    onOpen(_, ws) {
      const rawWs = ws.raw as ServerWebSocket;
      rawWs.subscribe(topic);
      console.log(`WebSocket server opened and subscribed to topic '${topic}'`);
    },
    onClose(_, ws) {
      const rawWs = ws.raw as ServerWebSocket;
      rawWs.unsubscribe(topic);
      console.log(
        `WebSocket server closed and unsubscribed from topic '${topic}'`
      );
    },
  }))
);

```

The behavior when a WebSocket connection is opened and closed is defined. When a connection is opened, it starts subscribing to a specific topic `synonymous-chat-room`, and when a connection is closed, it ends that subscription. In this app, when an anonymous user opens a page, it establishes a WebSocket communication and starts subscribing to the topic. This time, there is no limit to the number of people who can subscribe to the same topic.

Subscription to topics is not directly supported in Hono’s implementation, so we need to use Bun’s `ServerWebSocket`. Specifically, this is achieved by extending the `ws` in `onOpen`. Looking at the source code of Hono, `ws.raw` is the WebSocket of the Bun server, and by using this, we can handle Bun native WebSocket and implement topic subscription. Pass any topic name as a string to subscribe and unsubscribe.

Hono’s implementation of Bun WebSocket:
https://github.com/honojs/hono/blob/main/src/adapter/bun/websocket.ts

Official documentation for Bun WebSocket:
https://bun.sh/docs/api/websockets

### Backend: /messages Endpoint

server/index.ts

```typescript
import { zValidator } from '@hono/zod-validator';
const messages: Message[] = [];

const messagesRoute = app
  .get('/messages', (c) => {
    return c.json(messages);
  })
  .post(
    '/messages',
    zValidator('form', MessageFormSchema, (result, c) => {
      if (!result.success) {
        return c.json({ ok: false }, 400);
      }
    }),
    async (c) => {
      const param = c.req.valid('form');
      const currentDateTime = new Date();
      const message: Message = {
        id: Number(currentDateTime),
        date: currentDateTime.toLocaleString(),
        ...param,
      };
      const data: DataToSend = {
        action: publishActions.UPDATE_CHAT,
        message: message,
      };

      messages.push(message);
      server.publish(topic, JSON.stringify(data));

      return c.json({ ok: true });
    }
  )
  .delete('/messages/:id', (c) => {
    // Logic of message deletion
  });

export type AppType = typeof messagesRoute;
```

This messages resource supports GET, POST, DELETE methods. The GET method retrieves the message history of users subscribing to the same topic, the POST method creates a new message, and the DELETE method deletes a specific message. Here, I will focus on explaining POST /messages.

The server instance was created when creating the Bun server for use in this endpoint. By calling `publish()` on the `server` instance, we can broadcast to all clients subscribing to the same topic. According to the official document, it is also possible to broadcast to all subscribers of the topic, excluding the socket that is called `publish()`, but it is not used this time.

Also, like Express, we can implement middleware on the endpoint by passing a handler before processing the logic of the resource. This is part of `zValidator`. This allows us to handle `param` in a type-safe manner.

In `type AppType`, the type of API defined in `messagesRoute` is exported. This is utilized in the RPC implementation on the frontend described later. By sharing the API specification with the client, we can achieve a type-safe API implementation.

shared/types.ts

```typescript
import { z } from 'zod';

export const MessageFormSchema = z.object({
  userId: z.string().min(1),
  text: z.string().trim().min(1),
});
```

I used zod for the `MessageFormSchema`. By combining TypeScript and zod, we can implement more strict validation.

### Frontend: Setting up WebSocket

frontend/src/App.tsx

```typescript
const [messages, setMessages] = useState<Message[]>([]);

useEffect(() => {
  const socket = new WebSocket('ws://localhost:3000/ws');

  socket.onopen = (event) => {
    console.log('WebSocket client opened', event);
  };

  socket.onmessage = (event) => {
    try {
      const data: DataToSend = JSON.parse(event.data.toString());
      switch (data.action) {
        case publishActions.UPDATE_CHAT:
          setMessages((prev) => [...prev, data.message]);
          break;
        case publishActions.DELETE_CHAT:
          setMessages((prev) =>
            prev.filter((message) => message.id !== data.message.id)
          );
          break;
        default:
          console.error('Unknown data:', data);
      }
    } catch (_) {
      console.log('Message from server:', event.data);
    }
  };
  socket.onclose = (event) => {
    console.log('WebSocket client closed', event);
  };

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

```

I create a WebSocket client and connect it to the server. I define the behavior for when the WebSocket connection is opened, when a message is received, and when the connection is closed. In `socket.onmessage`, I use a switch statement to branch based on the action type received from the backend. This allows us to handle various use cases.

```typescript
// shared/constants.ts
export const publishActions = {
  UPDATE_CHAT: 'UPDATE_CHAT',
  DELETE_CHAT: 'DELETE_CHAT',
} as const;

// shared/types.ts
type PublishAction = (typeof publishActions)[keyof typeof publishActions];

export type Message = { id: number; date: string } & MessageFormValues;

export type DataToSend = {
  action: PublishAction;
  message: Message;
};
```

Here are the constants and type definitions used for WebSocket and message management. The `PublishAction type` infers the sum of the values of the `publishActions` object and extracts the enumeration type.

### Frontend: Sending Messages

frontend/src/App.tsx

```typescript
import { hc } from 'hono/client';
import type { AppType } from '@server/index';

const honoClient = hc<AppType>("http://localhost:3000");

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  try {
    const validatedValues = MessageFormSchema.parse(formValues);
    const response = await honoClient.messages.$post({
      form: validatedValues,
    });
    if (!response.ok) {
      throw new Error('Failed to send message');
    }

  } catch (error) {
    // Error Handling Logic
  }
};

```

In `handleSubmit`, I define the logic for sending a new message. When text is sent, it validates the input value and sends the new message as a POST request to the server.

What’s noteworthy is that I use the Hono client instead of the fetch API. I specify the `AppType` exported in the backend and the backend URL to `hc`, and define a type-safe `honoClient`. This enables the implementation of RPC. The gif below is a demo of TypeScript throwing a compile error when an incorrect data type value is passed to `$post`.

![RPC Gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ouwgw6mjj6nx8vosmlzq.gif)

This allows us to implement a type-safe API by combining `zod` and RPC.

```typescript
<form
  method="post"
  onSubmit={handleSubmit}
  className="flex items-center space-x-2"
>
  <input name="userId" defaultValue={formValues.userId} hidden />
  <input
    name="text"
    value={formValues.text}
    onChange={handleInputChange}
    className="flex-grow p-2 border border-gray-800 rounded-md bg-gray-800 text-white"
  />
  <button
    type="submit"
    className="px-4 py-2 bg-blue-500 text-white rounded-md"
  >
    Send
  </button>
</form>
```

The UI implementation is excerpted only for the message-sending form part. This configuration is simple, placing a message `input` tag and a message send button inside the `form` tag. When the send button is pressed, `handleSubmit` is triggered, and a request is sent to the backend.

That’s the introduction to the main implementation. Personally, I found that setting up WebSocket with Hono and Bun was not difficult, and I felt the difficulty level was equivalent to implementing WebSocket with Socket.io in Express. I was planning to write a post on WebSocket, but the implementation of a type-safe API using RPC with Hono also provided a very good development experience, so the post became long. I’m looking forward to future updates of Hono.

That is about it. Happy coding!