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/oncalls (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
msgpackrby 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
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");
});
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>
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/clientexposes an ES module build. If you encounter issues, use a bundler or import fromhttps://esm.sh/@bytesocket/client.
Step 4: Run It
- Start the server:
npx tsx server.ts - Open the HTML file in two browser tabs.
- 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,
});
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();
}
});
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();
}
});
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,
});
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",
});
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 forws(uses@bytesocket/server;wsincluded as a dependency) -
@bytesocket/uws– server adapter for uWebSockets.js (uses@bytesocket/server; requires separateuWebSockets.jsinstall) -
@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
- GitHub: a7med3ouda/bytesocket
-
npm:
- Client: @bytesocket/client
- Node server: @bytesocket/node
- uWS server: @bytesocket/uws
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)