Jspreadsheet and Express

Jspreadsheet Server can attach its WebSocket traffic to an HTTP server you create, so an Express application and the spreadsheet server share one process and one port.

Full example

Create the Node HTTP server yourself, mount Express on it, and hand the same instance to Jspreadsheet Server through the server option:

const server = require('@jspreadsheet/server');
const adapter = require('@jspreadsheet/server-redis');

const express = require('express');
const http = require('http');
const app = express();
const httpServer = http.createServer(app);

app.get('/test', (req, res) => {
    res.send('Hello world!');
});

// Load from ENV
require('dotenv').config();

// Jspreadsheet license
const license = {
    clientId: process.env.JSS_CLIENT,
    licenseKey: process.env.JSS_LICENSE
};

// Create Jspreadsheet Server
server({
    config: {
        cors: {
            origin: "*"
        },
    },
    port: 3000,
    server: httpServer,
    beforeConnect: async function(auth) {
        // Allow everyone
        return true;
    },
    beforeLoad: async function(guid, auth) {
        // Allow everything
        return true;
    },
    beforeChange: async function(guid, changes, auth) {
        // Allow everything
        return true;
    },
    load: async function(guid, auth, cachedConfiguration) {
        // Load the spreadsheet when is not already in cache
        if (! cachedConfiguration) {
            // Load the spreadsheet from the adapter
            cachedConfiguration = await adapter.load(guid, auth);
        }
        return cachedConfiguration;
    },
    change: async function(guid, changes, auth, onerror) {
        return await adapter.change(guid, changes, auth, onerror);
    },
    create: async function(guid, config, auth) {
        return await adapter.create(guid, config, auth);
    },
    destroy: async function(guid, auth) {
        return await adapter.destroy(guid, auth);
    },
    error: function(e) {
        console.error('Error', e)
    },
    license: license
});

How it works

Sharing one HTTP server and port

http.createServer(app) wraps the Express application in a plain Node HTTP server. With the server option set, Jspreadsheet Server attaches Socket.IO to that server and starts it on port. Both answer on port 3000: Express serves /test, Socket.IO handles the WebSocket traffic on its own path.

Do not call listen yourself

Jspreadsheet Server calls listen(port) on the server you pass in. Pass a server that is not yet listening and do not call app.listen() or httpServer.listen(): a second listen throws ERR_SERVER_ALREADY_LISTEN.

Route ownership

Socket.IO claims only its handshake path (/socket.io/ by default). Every other request reaches Express unchanged, so routes, static files and error handling work as usual.

Middleware behavior

Express middleware runs only for HTTP requests that Express handles. WebSocket traffic bypasses it: body parsers, sessions and loggers neither see nor interfere with it. Spreadsheet traffic is authenticated by the server's hooks, not by Express middleware.

Access control hooks

The three before* hooks decide access. The example allows everyone; in a real application, validate the auth object (for example a token issued by your login flow) and return false to reject. The client sends the token in its auth option on every interaction, so an Express login route and the hooks can share the same verification code.

Persistence with the Redis adapter

The load, change, create and destroy handlers delegate to the Redis adapter, so documents survive restarts. cachedConfiguration lets the server skip a storage read when the document is already in memory. Any adapter or custom handlers plug in the same way.

Serving the front end from Express

Express can also serve the pages that host the spreadsheet. The front end connects with the client extension to the same origin:

// Express serves the application pages
app.use(express.static('public'));
// In the front end served from /public
import jspreadsheet from 'jspreadsheet';
import client from '@jspreadsheet/client';

jspreadsheet.setExtensions({ client });

const remote = client.connect({
    url: window.location.origin,
    auth: { token },
});

Limitations

The REST API extension binds its own HTTP server

The REST API is provided by the api extension, which creates and binds its own HTTP server during license registration, replacing the server option. If you enable extensions: { api } and pass an Express httpServer, the REST server wins and the Express routes are not served.

To use both, run Express on one port and the REST-enabled Jspreadsheet Server on another, and route paths at a reverse proxy such as Nginx. Only the WebSocket-only setup on this page (no api extension) shares a port with Express.

Related pages