Express Integration

WSX provides seamless integration with Express.js through the @wsx-sh/express adapter, allowing you to add real-time WebSocket functionality to your Express applications.

Installation

npm install @wsx-sh/core @wsx-sh/express

Basic Setup

Quick Start

import { createExpressWSXServer } from "@wsx-sh/express";
import { html } from "@wsx-sh/core";
import express from "express";

// Create WSX server with Express adapter
const wsx = createExpressWSXServer();
const app = wsx.getApp();

// Add WSX handlers
wsx.on("hello", async (request, connection) => {
  return {
    id: request.id,
    target: request.target,
    html: html`<div>
      Hello from Express at ${new Date().toLocaleTimeString()}!
    </div>`,
  };
});

// Serve static files
app.use(express.static("public"));

// Regular Express routes
app.get("/", (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>WSX with Express</title>
      <script src="/wsx.js"></script>
    </head>
    <body>
      <div wx-config='{"url": "ws://localhost:3000/ws"}'>
        <div id="content">Click the button below</div>
        <button wx-send="hello" wx-target="#content">Say Hello</button>
      </div>
    </body>
    </html>
  `);
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Configuration

WSX Configuration

const wsx = createExpressWSXServer({
  // WebSocket path
  path: "/ws",

  // Connection limits
  maxConnections: 1000,

  // Message size limits
  maxMessageSize: 1024 * 1024, // 1MB

  // Heartbeat settings
  pingInterval: 30000,
  pongTimeout: 10000,

  // CORS settings
  cors: {
    origin: ["http://localhost:3000"],
    credentials: true,
  },
});

Express Middleware Integration

import cors from "cors";
import helmet from "helmet";
import compression from "compression";
import rateLimit from "express-rate-limit";

const wsx = createExpressWSXServer();
const app = wsx.getApp();

// Security middleware
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        connectSrc: ["'self'", "ws://localhost:3000", "wss://your-domain.com"],
      },
    },
  })
);

// CORS middleware
app.use(
  cors({
    origin:
      process.env.NODE_ENV === "production"
        ? ["https://your-domain.com"]
        : ["http://localhost:3000"],
    credentials: true,
  })
);

// Compression
app.use(compression());

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);

// Body parsing
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));

Authentication Integration

Session-Based Authentication

import session from "express-session";
import MongoStore from "connect-mongo";

// Session middleware
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({
      mongoUrl: process.env.MONGODB_URI,
    }),
    cookie: {
      secure: process.env.NODE_ENV === "production",
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    },
  })
);

// Authentication middleware
function requireAuth(req, res, next) {
  if (req.session.user) {
    next();
  } else {
    res.status(401).json({ error: "Authentication required" });
  }
}

// Apply to protected routes
app.use("/api/protected", requireAuth);

// WSX connection authentication
wsx.onConnection = (connection) => {
  // Extract session from connection request
  const sessionCookie = connection.request.headers.cookie;
  const sessionId = parseSessionId(sessionCookie);

  if (sessionId) {
    // Load session data
    const sessionData = getSessionData(sessionId);
    if (sessionData?.user) {
      connection.sessionData = { user: sessionData.user };
    }
  }
};

// Protected WSX handler
wsx.on("protected-action", async (request, connection) => {
  if (!connection.sessionData?.user) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Please log in to perform this action</div>`,
    };
  }

  return {
    id: request.id,
    target: request.target,
    html: `<div>Hello, ${connection.sessionData.user.name}!</div>`,
  };
});

JWT Authentication

import jwt from "jsonwebtoken";

// JWT middleware for HTTP routes
function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// Protected API routes
app.use("/api/protected", authenticateToken);

// WSX JWT authentication
wsx.onConnection = (connection) => {
  const token = extractTokenFromConnection(connection);

  if (token) {
    try {
      const user = jwt.verify(token, process.env.JWT_SECRET);
      connection.sessionData = { user };
    } catch (error) {
      console.error("Invalid JWT token:", error);
      connection.close();
    }
  }
};

function extractTokenFromConnection(connection) {
  // Extract from query parameter
  const url = new URL(connection.request.url, "http://localhost");
  const token = url.searchParams.get("token");

  if (token) return token;

  // Extract from authorization header
  const authHeader = connection.request.headers.authorization;
  return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
}

Database Integration

MongoDB with Mongoose

import mongoose from "mongoose";

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// User model
const UserSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  createdAt: { type: Date, default: Date.now },
});

const User = mongoose.model("User", UserSchema);

// WSX handlers with database operations
wsx.on("create-user", async (request, connection) => {
  try {
    const { username, email } = request.data;

    const user = new User({ username, email });
    await user.save();

    return {
      id: request.id,
      target: request.target,
      html: `<div class="success">User ${username} created successfully!</div>`,
    };
  } catch (error) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Failed to create user: ${error.message}</div>`,
    };
  }
});

wsx.on("get-users", async (request, connection) => {
  try {
    const users = await User.find().sort({ createdAt: -1 }).limit(10);

    const usersHtml = users
      .map(
        (user) => `
      <div class="user">
        <h3>${user.username}</h3>
        <p>${user.email}</p>
        <small>Joined: ${user.createdAt.toLocaleDateString()}</small>
      </div>
    `
      )
      .join("");

    return {
      id: request.id,
      target: request.target,
      html: usersHtml,
    };
  } catch (error) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Failed to load users</div>`,
    };
  }
});

PostgreSQL with Sequelize

import { Sequelize, DataTypes } from "sequelize";

// Database connection
const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialect: "postgres",
  logging: false,
});

// User model
const User = sequelize.define("User", {
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true,
    },
  },
});

// Sync database
sequelize.sync();

// WSX handlers
wsx.on("user-search", async (request, connection) => {
  try {
    const { query } = request.data;

    const users = await User.findAll({
      where: {
        [Op.or]: [
          { username: { [Op.iLike]: `%${query}%` } },
          { email: { [Op.iLike]: `%${query}%` } },
        ],
      },
      limit: 10,
    });

    const usersHtml = users
      .map(
        (user) => `
      <div class="user-result">
        <span class="username">${user.username}</span>
        <span class="email">${user.email}</span>
      </div>
    `
      )
      .join("");

    return {
      id: request.id,
      target: request.target,
      html: usersHtml || "<div>No users found</div>",
    };
  } catch (error) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Search failed</div>`,
    };
  }
});

Real-Time Features

Live Chat Application

// Chat message model
const MessageSchema = new mongoose.Schema({
  content: { type: String, required: true },
  username: { type: String, required: true },
  timestamp: { type: Date, default: Date.now },
});

const Message = mongoose.model("Message", MessageSchema);

// Send message handler
wsx.on("send-message", async (request, connection) => {
  try {
    const { content } = request.data;
    const username = connection.sessionData?.user?.username || "Anonymous";

    // Save message to database
    const message = new Message({ content, username });
    await message.save();

    // Broadcast to all connected clients
    const messageHtml = `
      <div class="message">
        <span class="username">${username}</span>
        <span class="timestamp">${message.timestamp.toLocaleTimeString()}</span>
        <div class="content">${content}</div>
      </div>
    `;

    wsx.broadcast("#chat-messages", messageHtml, "beforeend");

    // Clear sender's input
    return {
      id: request.id,
      target: "#message-input",
      html: `<input type="text" placeholder="Type a message..." />`,
      swap: "outerHTML",
    };
  } catch (error) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Failed to send message</div>`,
    };
  }
});

// Load recent messages
wsx.on("load-messages", async (request, connection) => {
  try {
    const messages = await Message.find()
      .sort({ timestamp: -1 })
      .limit(50)
      .reverse();

    const messagesHtml = messages
      .map(
        (msg) => `
      <div class="message">
        <span class="username">${msg.username}</span>
        <span class="timestamp">${msg.timestamp.toLocaleTimeString()}</span>
        <div class="content">${msg.content}</div>
      </div>
    `
      )
      .join("");

    return {
      id: request.id,
      target: request.target,
      html: messagesHtml,
    };
  } catch (error) {
    return {
      id: request.id,
      target: request.target,
      html: `<div class="error">Failed to load messages</div>`,
    };
  }
});

Live Dashboard

// Dashboard stats endpoint
app.get("/api/stats", authenticateToken, async (req, res) => {
  try {
    const stats = {
      totalUsers: await User.countDocuments(),
      onlineUsers: wsx.getConnectionCount(),
      totalMessages: await Message.countDocuments(),
      recentActivity: await getRecentActivity(),
    };

    res.json(stats);
  } catch (error) {
    res.status(500).json({ error: "Failed to fetch stats" });
  }
});

// Auto-update dashboard every 30 seconds
setInterval(async () => {
  try {
    const stats = {
      totalUsers: await User.countDocuments(),
      onlineUsers: wsx.getConnectionCount(),
      totalMessages: await Message.countDocuments(),
      recentActivity: await getRecentActivity(),
    };

    const dashboardHtml = `
      <div class="stats-grid">
        <div class="stat">
          <h3>Total Users</h3>
          <p>${stats.totalUsers}</p>
        </div>
        <div class="stat">
          <h3>Online Now</h3>
          <p>${stats.onlineUsers}</p>
        </div>
        <div class="stat">
          <h3>Total Messages</h3>
          <p>${stats.totalMessages}</p>
        </div>
      </div>
    `;

    // Broadcast to admin users
    const adminConnections = wsx
      .getConnections()
      .filter((conn) => conn.sessionData?.user?.role === "admin");

    adminConnections.forEach((conn) => {
      wsx.sendToConnection(conn.id, "#dashboard-stats", dashboardHtml);
    });
  } catch (error) {
    console.error("Dashboard update failed:", error);
  }
}, 30000);

File Upload Integration

Multer Integration

import multer from "multer";
import path from "path";

// Configure multer
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(
      null,
      file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
    );
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
  fileFilter: (req, file, cb) => {
    const allowedTypes = /jpeg|jpg|png|gif/;
    const extname = allowedTypes.test(
      path.extname(file.originalname).toLowerCase()
    );
    const mimetype = allowedTypes.test(file.mimetype);

    if (mimetype && extname) {
      return cb(null, true);
    } else {
      cb(new Error("Only images are allowed"));
    }
  },
});

// File upload endpoint
app.post(
  "/api/upload",
  authenticateToken,
  upload.single("image"),
  (req, res) => {
    if (!req.file) {
      return res.status(400).json({ error: "No file uploaded" });
    }

    res.json({
      filename: req.file.filename,
      originalName: req.file.originalname,
      size: req.file.size,
      url: `/uploads/${req.file.filename}`,
    });
  }
);

// Serve uploaded files
app.use("/uploads", express.static("uploads"));

// WSX handler for file upload notification
wsx.on("file-uploaded", async (request, connection) => {
  const { filename, originalName } = request.data;

  // Broadcast to relevant users
  const notificationHtml = `
    <div class="upload-notification">
      <p>File uploaded: ${originalName}</p>
      <img src="/uploads/${filename}" alt="${originalName}" style="max-width: 200px;" />
    </div>
  `;

  wsx.broadcast("#file-notifications", notificationHtml, "afterbegin");

  return {
    id: request.id,
    target: request.target,
    html: `<div class="success">File uploaded successfully</div>`,
  };
});

Error Handling

Global Error Handler

// Express error handler
app.use((error, req, res, next) => {
  console.error(error.stack);

  if (error.name === "ValidationError") {
    return res.status(400).json({ error: error.message });
  }

  if (error.name === "CastError") {
    return res.status(400).json({ error: "Invalid ID format" });
  }

  res.status(500).json({ error: "Internal server error" });
});

// WSX error handler
wsx.onError = (error, connection) => {
  console.error("WSX error:", error);

  if (connection) {
    connection.send(
      JSON.stringify({
        type: "error",
        message: "An unexpected error occurred",
      })
    );
  }
};

// 404 handler
app.use("*", (req, res) => {
  res.status(404).json({ error: "Route not found" });
});

Production Deployment

PM2 Configuration

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "wsx-express-app",
      script: "server.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "development",
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: "production",
        PORT: 8080,
      },
      error_file: "./logs/err.log",
      out_file: "./logs/out.log",
      log_file: "./logs/combined.log",
      time: true,
    },
  ],
};

Docker Setup

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

RUN mkdir -p uploads logs

EXPOSE 3000

CMD ["npm", "start"]

Nginx Configuration

upstream wsx_app {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name your-domain.com;

    # WebSocket proxy
    location /ws {
        proxy_pass http://wsx_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Regular HTTP proxy
    location / {
        proxy_pass http://wsx_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Best Practices

  1. Use Environment Variables: Store configuration in environment variables
  2. Implement Proper Authentication: Secure your WebSocket endpoints
  3. Handle Errors Gracefully: Implement comprehensive error handling
  4. Use Middleware: Leverage Express middleware for common functionality
  5. Monitor Performance: Track response times and connection counts
  6. Scale Appropriately: Use clustering or load balancing for high traffic
  7. Log Everything: Implement comprehensive logging for debugging

Next Steps