Skip to content

Advanced Usage

Advanced patterns for building sophisticated WebSocket applications with Bun WebSocket Router.

Discriminated Unions

With the factory pattern (v0.4.0+), you can now use Zod's discriminated unions to create type-safe message handlers:

typescript
import { z } from "zod";
import { createMessageSchema } from "bun-ws-router/zod";

const { messageSchema } = createMessageSchema(z);

// Define individual message schemas
const TextMessage = messageSchema("TEXT", {
  content: z.string(),
  channelId: z.string(),
});

const ImageMessage = messageSchema("IMAGE", {
  url: z.url(),
  channelId: z.string(),
  width: z.number(),
  height: z.number(),
});

const VideoMessage = messageSchema("VIDEO", {
  url: z.url(),
  channelId: z.string(),
  duration: z.number(),
});

// Create a discriminated union
const MediaMessage = z.discriminatedUnion("type", [
  TextMessage,
  ImageMessage,
  VideoMessage,
]);

// Type-safe message handling
function handleMediaMessage(message: z.infer<typeof MediaMessage>) {
  switch (message.type) {
    case "TEXT":
      // TypeScript knows payload has { content, channelId }
      console.log("Text:", message.payload.content);
      break;

    case "IMAGE":
      // TypeScript knows payload has { url, channelId, width, height }
      console.log("Image:", message.payload.url, message.payload.width);
      break;

    case "VIDEO":
      // TypeScript knows payload has { url, channelId, duration }
      console.log("Video:", message.payload.url, message.payload.duration);
      break;
  }
}

// Use in router - register individual handlers
router
  .onMessage(TextMessage, (ctx) => {
    // Handle text specifically
  })
  .onMessage(ImageMessage, (ctx) => {
    // Handle images specifically
  })
  .onMessage(VideoMessage, (ctx) => {
    // Handle videos specifically
  });

This pattern is useful for protocol versioning, command/query separation, event sourcing, and complex state machines.

Router Composition

Compose multiple routers to organize your application into modules:

typescript
import { z } from "zod";
import { WebSocketRouter, createMessageSchema } from "bun-ws-router/zod";

const { messageSchema } = createMessageSchema(z);

// Authentication router
const authRouter = new WebSocketRouter()
  .onMessage(LoginMessage, handleLogin)
  .onMessage(LogoutMessage, handleLogout)
  .onMessage(RefreshTokenMessage, handleRefresh);

// Chat router
const chatRouter = new WebSocketRouter()
  .onMessage(SendMessageMessage, handleSendMessage)
  .onMessage(EditMessageMessage, handleEditMessage)
  .onMessage(DeleteMessageMessage, handleDeleteMessage);

// Admin router
const adminRouter = new WebSocketRouter()
  .onMessage(KickUserMessage, handleKickUser)
  .onMessage(BanUserMessage, handleBanUser)
  .onMessage(MuteUserMessage, handleMuteUser);

// Main router combining all
const mainRouter = new WebSocketRouter()
  .addRoutes(authRouter)
  .addRoutes(chatRouter)
  .addRoutes(adminRouter)
  .onOpen(handleConnection)
  .onClose(handleDisconnection);

TIP

For complex applications, consider organizing routers by feature domain (auth, chat, notifications) rather than by message type.

Authentication Middleware

Create reusable authentication checks:

typescript
import { publish } from "bun-ws-router/zod/publish";

// Authentication check helper
function requireAuth<T>(
  handler: (ctx: MessageContext<T>) => void | Promise<void>,
): (ctx: MessageContext<T>) => void | Promise<void> {
  return async (ctx) => {
    if (!ctx.ws.data.authenticated) {
      ctx.send(ErrorMessage, {
        code: "AUTHENTICATION_FAILED",
        message: "Authentication required",
      });
      return;
    }
    await handler(ctx);
  };
}

// Use with router
router.onMessage(
  ProtectedMessage,
  requireAuth((ctx) => {
    // Handler only runs if authenticated
    publish(ctx.ws, "updates", UpdateMessage, ctx.payload);
  }),
);

Custom Connection Data

Store and access application-specific data per connection:

typescript
interface UserData {
  userId: string;
  username: string;
  authenticated: boolean;
  joinedRooms: Set<string>;
}

const router = new WebSocketRouter<UserData>();

router
  .onOpen((ctx) => {
    // Initialize connection data
    ctx.ws.data.authenticated = false;
    ctx.ws.data.joinedRooms = new Set();
  })
  .onMessage(AuthMessage, (ctx) => {
    // Set authenticated user data
    ctx.ws.data.userId = "user-123";
    ctx.ws.data.username = "Alice";
    ctx.ws.data.authenticated = true;
  })
  .onMessage(JoinRoomMessage, (ctx) => {
    // Track joined rooms
    ctx.ws.data.joinedRooms.add(ctx.payload.roomId);
    ctx.ws.subscribe(`room:${ctx.payload.roomId}`);
  });

Rate Limiting

Implement per-client rate limiting:

typescript
const rateLimits = new Map<string, { count: number; resetAt: number }>();

function checkRateLimit(clientId: string, maxPerMinute: number): boolean {
  const now = Date.now();
  const limit = rateLimits.get(clientId);

  if (!limit || now > limit.resetAt) {
    rateLimits.set(clientId, { count: 1, resetAt: now + 60000 });
    return true;
  }

  if (limit.count >= maxPerMinute) {
    return false;
  }

  limit.count++;
  return true;
}

router.onMessage(ChatMessage, (ctx) => {
  if (!checkRateLimit(ctx.ws.data.clientId, 10)) {
    ctx.send(ErrorMessage, {
      code: "RATE_LIMIT_EXCEEDED",
      message: "Too many messages. Please slow down.",
    });
    return;
  }

  publish(ctx.ws, "chat", ChatMessage, ctx.payload);
});