DEV Community

Erik Vullings
Erik Vullings

Posted on

Client-server communication setup via REST and WebSockets

TLDR: In production, or when your client-server app is running in a Docker container, the server port is typically configured outside of your control. So how can the client still discover the server's port dynamically in order to access REST resources or setup a WebSocket connection? The approach taken here is too serve the client as a static resource via the server, use document.location.href to infer the location of the server, and fallback to the hard-coded development settings if that fails.


When developing client-server web applications, I normally use a node.js-based server and a Single-Page Application client. Typically, the server provides some REST endpoints, for example to fetch some lists, and also allows for real-time WebSocket communication.

This works pretty easy during development: the server offers a REST and WebSocket interface on a configured port, let's say 1234, and the client connects to it http://localhost:1234/[REST-RESOURCE] or io(http://localhost:1234). As the client may be served by a development server (like webpack-dev-server or parcel) to have hot reloading, you only need to enable CORS on the server to allow the client to connect to it. On the server, if you use nest.js, it may be as simple as

import * as bodyParser from 'body-parser';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, { cors: true });
  app.use(bodyParser.json({ limit: '10mb' }));
  app.use(bodyParser.urlencoded({ limit: '10mb' }));

  const port = 1234;
  await app.listen(port, () => {
    console.log(`Server is listening on port ${port}.`);
  });
}
bootstrap(); 

This works all well during development, but in production or in a Docker container, we most likely need to specify the port of the server too, so instead of hard-coding the port, we get it from the environment by replacing const port = 1234; with:

  const port = process.env.MY_SERVER_PORT || 1234;

Since now you no longer control the port, how do you tell your client where your server is located? One solution is the following: when building the application for production, the generated output is moved to the server's public folder used for serving static content, i.e.

  app.use(express.static(path.join(process.cwd(), 'public')));

This enables the client to discover the server using window.location or document.location.href. As the client is not aware whether it is running in production mode or in development mode, it assumes it is running in production mode, and if the connection fails, it switches seamlessly to development mode. So for socket.io the client first tries production mode, and if this fails, i.e. it receives a connect_error, it tries development mode:

let socket: SocketIOClient.Socket;

const setupSocket = (productionMode = true) => {
  socket = productionMode ? io() : io('http://localhost:1234');
  socket.on('connect_error', () => {
    socket.close();
    if (productionMode) {
      socket = setupSocket(false);
    } else {
      console.error('event', data);
    }
  });
};

For the REST services, a similar approach is taken: first try document.location.href to reach the server, and when that fails, try the hard-coded development location. Something like

  public async loadList(): Promise<T[] | undefined> {
    try {
      const result = await m
      .request<T[]>({
        method: 'GET',
        url: this.baseUrl,
        withCredentials,
      });
      return result;
    } catch {
      if (this.developmentMode) {
        throw Error('Help');
      }
      // The initial value was based on production mode, so when you fail, switch
      this.baseUrl = this.createDevModeUrl();
      return this.loadList();
    }
  }

If you are dealing with a reverse proxy like nginx, traefik or redbird, you can even get a bit fancier using the following snippet, grabbing everything before the first hash tag.

const getRootUrl = () => {
  // Regex matching everything until the first hash symbol, so should also be able to deal with route rewriting...
  const regex = /https?:\/\/.*(?=\/#)/i;
  const route = document.location.href;
  const m = route.match(regex);
  return (m && m.length === 1) ? m[0].toString() : '';
};

In an actual application, I normally store the development or production mode in the application state or store, so I only fail once per connection.

This solution, I profess, is not very elegant, and requires a bit of code to set it up properly. Also, in development, you most likely get one or two warnings due to failed communication with the server, as it assumes it is running in production mode (still, better than to have these errors while running in production). Therefore, I am happy to hear your suggestions or improvements.

Oldest comments (0)