WS + API: A NodeJS Server
Designing a Node Server with Web Sockets and RESTful API
- Part 1: One Server To Rule Th…Most of Them
- Part 2: WS + API: Getting Started
- Part 3: WS + API: Enter the Index.ts
- Part 4: Interlude: Protocols
- Part 5: WS + API: Web Socket Handler
- Part 6: WS + API: Getting the Message
- Part 7: Interlude: Redis & Redis-OM
- Part 8: WS + API: Actions Speak Louder Than Code
- Part 9: From Node to Deno
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.