WS + API: A NodeJS Server

Designing a Node Server with Web Sockets and RESTful API

As with many Node apps, our entry point is going to be an index.ts file located in the /src directory. In previous attempts at this project I moved fast and didn’t spend much time thinking about code organization and the result was that most everything the app needed to get started existed in this one file. On this refactor, though, I wanted to clean things up, keep the web sockets apart from the endpoints, and only task the index file with getting the server running.

// Set up the .env file we'll be using
require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` });

// Modules
const config = require('better-config');
const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');

// Load the config
config.set('../config.json');

// Set up the server component. This is used for both
// web sockets and the HTTPS endpoints
const app= express();
const server = http.createServer(app);
const wsServer = new WebSocketServer({server})

// Split out to deal with the API endpoints
//  and the web socket processing
const SocketServer = require('./Websockets');
const APIServer = require('./API');

// Get the port from the config file,
// or specify a fallback
const port = config.get('application.port') || 8088;
server.listen(port, ()=>{
    console.log(`Server is listening on port ${port}`)
})

// Init our WS and API extensions
const _socketServer = new SocketServer(wsServer);
_socketServer.Init();

const _apiServer = new APIServer(app);
_apiServer.Init();

So far, this is the “server”. Let’s take a look.

.env Setup

I’m using dotenv to load the environment variable file. Remember that this is going to contain information that will be compiled into the app and contains information like credentials for connecting to databases. The options only supply a value for path, and that value is determined by the setting of NODE_ENV which, in this case, is “development” as specified by the script defined in package.json that we execute when we start the server using nodemon: "start": "SET NODE_ENV=development&& nodemon ./src/index.ts".

Modules

There are four modules being loaded at this point: config from Better Config that loads our DB choice information, express from Express which is our routing engine, http which is native to Node and handles web requests and responses, and WebSocketServer which provides web socket functionality.

config Loading

The line config.set('../config.json') loads the config file from the root of the app. We can then access contents of the config file throughout the application.

Server Components

For this project, we’re mixing a few different parts which have different responsibilities into one “server”. The definition of app creates an instance of an Express web routing server. The app will define these routes to handle traffic based on request forms and headers, actions will be taken, and responses will be returned.

By itself, the Express app doesn’t do much, so we’re joining forces with Node’s http library to create the server object. As the server receives GET, POST, and other known web action verb requests via HTTP(S), they will be routed through to app which is our instance of Express.

We also define a WebSocketServer instance, passing the instance of server as an argument. The web socket protocol uses ‘ws’ rather than ‘http’ or ‘https’, but our server is still listening on the same port. We could modify the arguments when defining the wsServer to have web socket traffic on a different port than the one used for the API traffic — and indeed, that may be the best practice to do so, which I’ll need to work in — but for now, any traffic that specifies the ‘ws’ protocol will be transferred through the wsServer instance.

Defining our Handlers

Once we have the servers defined, the app loads in the two branch handlers for the app, SocketServer for the web socket traffic, and APIServer for the Express routing. These files will be discussed in a later post, but know that these are where the heavy lifting will begin.

Opening the Doors

The last part of the operational server is to define the port and start listening for traffic. The port value is stored in the config.json file that we loaded using the config definition at the top of the file. port is a child node of application and the dot-syntax allows us to access that value. If for some reason the port is not defined in the config file, we’ll fall back to 8088 which I chose in memory of the Days of Ye Olden Processors.

server.listen opens the specified port and waits for connections.

Answering the Call

When talking about traffic handlers, we will either receive a web socket or web protocol request. The last lines of the file set up the handlers for both web sockets and web traffic. Requests will be picked up by the appropriate branch and dealt with accordingly.

Conclusion

If you try to recreate this project based on what we’ve looked at so far, you’ll get all kinds of errors because we’re missing the definitions of the traffic handlers, but if you omit their definitions and the last four lines of code, you will at least be able to get the server to spit out the “Server is listening on port XXXX” message in the console. Attempting to hit the server via browser or an app like Postman will do nothing, though, since the server doesn’t yet know how to handle incoming requests.

In the next post, we’ll go over what I’ve got for the more complicated of the two branches, the web sockets.

Series Navigation<< WS + API: Getting StartedInterlude: Protocols >>

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.