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."
}
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')
}
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])
})
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
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')
}
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)
}
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."
}
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"
}
]
}
}
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?
Top comments (2)
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!
Glad to hear it helps you! Thanks for reading and using it.