DEV Community

loading...
Cover image for Node Express and WebSockets (WS) Boiler Plate

Node Express and WebSockets (WS) Boiler Plate

Garret
Web developer & Software engineer with a focus in building tools for content creators
・4 min read

In this post I will be sharing with you some of the practices I follow, and how I create my API's using Node, Express, and WebSockets. I have recently publicly published the boiler plate that I use at https://github.com/itsUnsmart/express-ws-boiler so I decided to go over it.

Project Structure

The main thing I think about when setting up my projects is readability. If I cannot read through the project structure easily to find what I need it can be a pain.

In order to make things easy I split everything into its own directory. The Express API goes into the web directory. The WebSocket goes into the sockets directory. Finally any helpers I made need go into the helpers directory. In this example I have only one index.js file in the helpers, however, in bigger projects I recommend making multiple files with a more specific name to the type of helper functions if needed.

Simplified:

structure: {
  "/helpers": "Any helpers needed.",
  "/sockets": "Anything related to the websocket server.",
  "/web":     "Anything related to the express web server."
}
Enter fullscreen mode Exit fullscreen mode

Express Specifics

Specific to Express I have one main index.js file which starts the server, and sets up all the routing, as well as any other configuration needed. In order to make the routes in a clean structure I create a routes folder which contains an index.js that will parse out the routes into a single object for the main server file.
Example:

module.exports = {
  example: require('./example')
}
Enter fullscreen mode Exit fullscreen mode

In the above example it takes a route file called example.js and maps it to the "/example" path. The way the mapping works in the main index.js file is it require the route index file, and runs over the object keys.

const routes = require('./routes')
Object.keys(routes).forEach(key => {
  app.use(`/${key}`, routes[key])
})
Enter fullscreen mode Exit fullscreen mode

This means now any request which has example in the path will get routed to the code in the example.js file.
Meaning our simple hello world file:

const { Router } = require('express')
const router = Router()

router.get('/', (req, res) => {
  return res.status(200).json({
    hello: 'world'
  })
})

module.exports = router
Enter fullscreen mode Exit fullscreen mode

Will run when we go to http://localhost:3000/example and display:
{"hello": "world"}.

WebSocket Specifics

Specific to WS I have one main index.js file which starts the server, and sets up all the methods, as well as any other configuration needed. In order to make the methods in a clean structure I create a methods folder which contains an index.js that will parse out the methods into a single object for the main server file. This file is identical to the Express equivalent in the routes folder.

module.exports = {
  example: require('./example')
}
Enter fullscreen mode Exit fullscreen mode

The difference is now in order to call the method I look for the method to call in the WebSocket message inside of the method parameter (Note that I code all WebSocket message to be JSON).
Example:

const Methods = require('./methods')

// inside ws.on("message")
if (typeof data.method === 'string' && Methods[data.method]) {
  Methods[data.method](WSS, ws, data)
}
Enter fullscreen mode Exit fullscreen mode

Now if we were to connect to the socket and send the following message:
{"method": "example"} it would run our example method and return a response containing {"hello": "world"}.

Full structure

After this out full project structure would look like so:

structure: {
  "helpers": {
    "index.js": "The main file combining all other helper files if any."
  },
  "sockets": {
    "index.js": "The main server and configuration file.",
    "methods": {
      "index.js": "The main methods file which combines all methods."
    }
  },
  "web": "Same as sockets replacing methods with routes."
}
Enter fullscreen mode Exit fullscreen mode

Errors

After working on projects that integrate with 3rd party services I truly understand how useful good error messages can be. I always make sure to give as much detail into why an error happened so anyone using it can understand what went wrong.

I do this by giving my errors an easy to read error message, a machine readable error code useful for doing switch statements on. As well as context into the error(s) that occured.

Example of a 404 error:

{
  type: "error",
  message: "Not Found",
  code: "not_found",
  context_info: {
    errors: [
      {
        reason: "invalid_path",
        message: "The requested path could not be found",
        data: "/invalid/path",
        location: "path"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This error may be a bit overkill for a simple 404 error, however, it gives a user or even yourself a good insight into exactly what went wrong in the request. This is very useful, and makes debugging super easy.

Easy Install

Would you like to use this boiler plate in your projects? The easiest way to install it into your projects is by using degit.
Run the command degit itsUnsmart/express-ws-boiler, and it will clone the repository into your current directory.

Conclusion

That is the basics for how I structure my Node, Express, and WebSocket applications, and how I handle things such as errors. I am curious how do you structure your projects, and what do you like or dislike about how I structure mine?

Discussion (2)

Collapse
trainingmontage profile image
Jeff Caldwell

I'm new to Express and I've been looking around for good ways to structure Express apps with http & websockets.

This is a nice, easy to follow structure that I'm sure helps to spin up apps quickly. Thanks for sharing this!

Collapse
garretharp profile image
Garret Author

Glad to hear it helps you! Thanks for reading and using it.