DEV Community

Ahmed Ouda
Ahmed Ouda

Posted on

Build a Real-Time Chat with ByteSocket: The Fully-Typed WebSocket Library

Build a Real-Time Chat with ByteSocket

I've been building real‑time features for years, and I always wanted a WebSocket library that felt like an extension of TypeScript – one where every emit and on is fully typed, rooms feel native, and auth "just works" without a ton of boilerplate. That's why I created ByteSocket: a modern WebSocket client and server with first‑class TypeScript support, automatic reconnection, middleware, and binary serialization built in.

In this tutorial, we'll build a simple, fully typed chat server and client from scratch. By the end, you'll have a working app and a solid grasp of how ByteSocket can streamline your real‑time development.

Why ByteSocket?

Before we code, let me highlight what makes ByteSocket different:

  • Full end‑to‑end type safety – define your event map once, and all emit/on calls (including rooms) are typed.
  • Rooms with bulk operations – join, leave, and emit to multiple rooms in one go.
  • Authentication – static or async token flow with configurable timeouts.
  • Middleware – global and per‑room middleware chains on server (like Express, but for WebSockets).
  • Automatic reconnection – exponential backoff, jitter, and automatic room re‑join.
  • Binary serialization – uses msgpackr by default (smaller payloads), with JSON option if needed.
  • Fully tested – comprehensive Vitest suite covering connection lifecycle, messaging, rooms, auth, middleware, and heartbeat across all server adapters.

Now, let's build.

Step 1: Install the Packages

You'll need the server (Node.js) and the client (browser/Node).

# Server – @bytesocket/node already includes the ws library as a dependency, no extra install needed
npm install @bytesocket/node

# Or use the high‑performance uWebSockets.js adapter (requires uWebSockets.js peer dependency)
npm install @bytesocket/uws

# Client – runs in browser and Node.js
npm install @bytesocket/client
Enter fullscreen mode Exit fullscreen mode

We'll use @bytesocket/node to keep things simple.

Step 2: Create the Server

Create a file server.ts:

import http from "node:http";
import { ByteSocket, SocketEvents } from "@bytesocket/node";

// 1. Define your event map
type ChatEvents = SocketEvents<{
  "chat:message": { text: string };
  "user:joined": { userId: string };
  "user:left": { userId: string };
}>;

// 2. Create the server instance
const server = http.createServer();
const io = new ByteSocket<ChatEvents>({ debug: true });

// 3. Handle connection open
io.lifecycle.onOpen((socket) => {
  console.log(`${socket.id} connected`);
  socket.rooms.join("lobby"); // auto‑join a default room

  // Notify everyone
  socket.broadcast("user:joined", { userId: socket.id });
});

// 4. Listen for chat messages and re‑broadcast to the room
io.on("chat:message", (socket, data) => {
  console.log(`${socket.id} says: ${data.text}`);
  socket.rooms.emit("lobby", "chat:message", data);
});

// 5. Handle disconnection
io.lifecycle.onClose((socket) => {
  console.log(`${socket.id} disconnected`);
  socket.broadcast("user:left", { userId: socket.id });
});

// 6. Attach to the HTTP server and listen
io.attach(server, "/ws");
server.listen(3000, () => {
  console.log("Server listening on port 3000");
});
Enter fullscreen mode Exit fullscreen mode

What's happening?

  • SocketEvents<{...}> gives us full type inference – io.on("chat:message", ...) will only accept that exact event name and payload shape.
  • socket.rooms.join("lobby") adds the socket to a room.
  • socket.broadcast(...) sends to everyone except the sender.
  • socket.rooms.emit("lobby", ...) sends only to that room.
  • No manual serialization – ByteSocket handles JSON or MessagePack automatically.

Step 3: Create the Client

Create a simple HTML file with a script module, or a Node.js client if you prefer. Here's a browser example:

<!DOCTYPE html>
<html>
<body>
  <input id="msg" type="text" placeholder="Type a message…" />
  <button id="send">Send</button>
  <ul id="log"></ul>

  <script type="module">
    import { ByteSocket } from "https://unpkg.com/@bytesocket/client?module";
    // For Node.js: import { ByteSocket } from "@bytesocket/client";

    const socket = new ByteSocket("ws://localhost:3000/ws");

    // Log messages from the server
    socket.on("chat:message", (data) => {
      const li = document.createElement("li");
      li.textContent = data.text;
      document.getElementById("log").appendChild(li);
    });

    socket.on("user:joined", (data) => {
      console.log(`User ${data.userId} joined`);
    });

    // Send a message when the button is clicked
    document.getElementById("send").addEventListener("click", () => {
      const input = document.getElementById("msg");
      socket.emit("chat:message", { text: input.value });
      input.value = "";
    });
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note: When using <script type="module"> directly, TypeScript types aren't enforced. For full type safety, use a bundler like Vite or a Node.js client. The unpkg URL above works because @bytesocket/client exposes an ES module build. If you encounter issues, use a bundler or import from https://esm.sh/@bytesocket/client.

Step 4: Run It

  1. Start the server: npx tsx server.ts
  2. Open the HTML file in two browser tabs.
  3. Type a message – both tabs will see it instantly.

That's it! In less than 40 lines of server code, you have a fully functional, type‑checked chat.

What Else Can ByteSocket Do?

The library has many more capabilities that you can layer on as needed:

Authentication

Add a token check on the server:

const io = new ByteSocket({
  auth: (socket, data, callback) => {
    if (data.token === "secret") {
      callback({ userId: 1 }); // attached to socket.payload
    } else {
      callback(null, new Error("Invalid token"));
    }
  },
  authTimeout: 8000,
});
Enter fullscreen mode Exit fullscreen mode

Middleware

ByteSocket provides two levels of middleware:

Global Middleware (io.use) – runs on every incoming user message, before it reaches any room or global listener. Perfect for logging, rate limiting, or authentication checks.

io.use((socket, ctx, next) => {
  console.log(`Incoming event: ${ctx.event} from ${socket.id}`);
  if (socket.locals.rateLimited) {
    next(new Error("Rate limited"));
  } else {
    next();
  }
});
Enter fullscreen mode Exit fullscreen mode

Room Middleware – scoped to a specific room+event combination. You can block a message before it gets broadcast.

io.rooms.on("lobby", "chat:message", (socket, data, next) => {
  if (data.text.includes("badword")) {
    next(new Error("Profanity not allowed"));
  } else {
    next();
  }
});
Enter fullscreen mode Exit fullscreen mode

Global middleware runs first, then room middleware, and finally the actual event listeners. This gives you fine‑grained control over the message pipeline.

Reconnection (client side)

Automatic reconnection is on by default. You can tweak it:

const socket = new ByteSocket("ws://localhost:3000/ws", {
  reconnection: true,
  maxReconnectionAttempts: 10,
  reconnectionDelay: 1000,
});
Enter fullscreen mode Exit fullscreen mode

Binary Serialization

Smaller payloads with MessagePack – it's the default! Switch to JSON if you need debugging:

const socket = new ByteSocket("ws://localhost:3000/ws", {
  serialization: "json",
});
Enter fullscreen mode Exit fullscreen mode

Under the Hood

ByteSocket is split into several packages, each with a clear responsibility:

  • Core & Shared Abstractions
    • @bytesocket/core – shared type definitions and base class
    • @bytesocket/server – shared server abstractions and test utilities
  • Transport‑Specific Implementations
    • @bytesocket/node – server adapter for ws (uses @bytesocket/server; ws included as a dependency)
    • @bytesocket/uws – server adapter for uWebSockets.js (uses @bytesocket/server; requires separate uWebSockets.js install)
    • @bytesocket/client – browser/Node client (uses @bytesocket/core)

All of them are fully typed and share the same event map interface, so your types work everywhere. The codebase is battle‑tested with a shared Vitest suite that runs identical tests across both server transports – covering every lifecycle, room operation, auth flow, and edge case.

What’s Next?

The library is under active development. Here’s a peek at the roadmap:

  • Horizontal scaling adapter – built‑in support for socket.io‑style adapters (starting with Redis) so you can run multiple server instances and still broadcast across rooms seamlessly.
  • .NET server – a native server implementation using System.Net.WebSockets, bringing ByteSocket’s typed events to the .NET ecosystem.
  • Improved documentation & examples – more tutorials, real‑world examples, and best practices guides.

If there’s a feature you’d love to see, let me know – pull requests and suggestions are always welcome!

Why I Built It

I wanted a WebSocket library that:

  • Doesn't force me to give up TypeScript
  • Works the same on client and server
  • Includes rooms, auth, and middleware out of the box
  • Has minimal dependencies and a modern codebase

ByteSocket is my answer to that wishlist. It's MIT‑licensed and actively maintained.

Try It Yourself

If you build something cool with it, I'd love to hear! Drop a star on GitHub, open an issue, or connect with me on LinkedIn. Let's make real‑time TypeScript great together.

Happy coding! 🚀

Top comments (0)