Plenty of guidelines on how to write Node.js applications or applications in general have already been written. Not only are there a lot of them, but most are so specific that they can't be applied everywhere, or at least it doesn’t make sense to. There are only a few that are generally applicable. And those that, conversely, provide significant added value require their specific environment.
Thus, generalizing excellent advice on how to do something is challenging. But that's not the only way to determine a direction. If we don't know how to say where to go, let's instead consider where not to go. A list of such guidelines, on the other hand, is easily graspable and has a much broader application.
Use Default Readme
You know the story: you fork a template or a different project, or you use some create-x-app script to set up your codebase, and your project has the cloned readme. It works until you start working on the project. Now when every new coming developer gets confused, you can be proud to welcome them to the codebase by saying "Oh, don’t mind that readme".
Use Non-Standard Script Names in Your Package.json
There are some npm-reserved script names like start, test, etc. When working with TypeScript, there is probably a script to build your application. But don’t name this one
compile as usual, that is too easy to guess. Be more creative so that everyone has to check for the scripts in package.json and the implementation on their own.
Use TypeScript But Not Types
You’ve made a great decision to build your apps with TypeScript, so you can take advantage of its typing capabilities. This has made your code easily maintainable in the long run, you catch lots of errors before shipping it into production. But don’t worry, this can be fixed as well. TypeScript has this beautiful type
any representing anything: a number, an undefined, null, a function, an object with any properties, etc. Whenever you just don’t feel like typing that variable, just cast it to
any. It works best if you are creating higher-level abstractions, e.g., Gateway, so the developers using it will have to type it themselves or have to go through the implementation.
Add Comments Just to Parrot the Code
Comment the code they said. So from now on whenever you are writing a comment to describe a variable or a function, just copy the variable or function name and just split the words into a phrase. Best to give a few examples for this one: “loggerDefaultLevel” receives a comment “logger default level”, or if you feel fancy, you can try something like “The default level of a logger”. If you use functions named like “processItems” (at this point I am sure you are going to master this discipline as well!), the comment “Processes items” will do the trick. An important thing to keep in mind is not to include anything that is not stated in the code already.
Use Same Names For Different Things
Whether a variable or a function name, be sure to name them almost the same but with different meanings. In untyped languages, you can get away with variables and functions, but for typed languages like TypeScript, you have to go one step further to find a proper place for your mischief – like environment variables, a common way of configuring backend apps. Examples: You can introduce a CACHE_EXPIRATION variable to represent milliseconds for a blogs module, and CACHE_USER_EXPIRATION to represent seconds for the user module. XPARTY_SERVICE_URL which is expected to hold scheme, domains and optionally a port (e.g. https://example.com), but don’t forget to give it a brother for service Y in a form of YPARTY_SERVICE_URL, which is expected to hold everything as the previous one, but should include a specific pathname too (e.g. https://example.com/a/b/c)! If you have a culture of commenting on environment variables somewhere, this one can be beautifully combined with the previous tip.
Whichever database you are using, at some point, you will probably be selecting some data from it. The best way to do this is to always query for all the record properties, even if you need just one, like
id. Start your new project with this in your mind. This way you won’t have to change that much code when adding a new property to a database entity.
The following tips are all related to logging, so I grouped them together, but it is important to tell the difference between each of them. Heads-up: Most of the tips are Google Cloud Platform (GCP) – specific.
Log too little data. Common logger properties these days are that they allow you to log messages and metadata as well – on GCP you log this as JSON, and Cloud Logging will parse these so you can query the logs later.
Keep in mind that you can never log everything, but a polite web server application is expected to have some logging implemented so you can observe what it does. It is about finding a balance between these two. With this in mind, you can, for example, start printing “User logged in” logs every time a user logs in to your application. so no one knows who logged in or how, but that someone logged in. Nice.
Log too much with internal objects. Logging too little is good, but the opposite is even better. If you have been working with Node.js for some time, I am sure you’ve come across streams or buffers – oh boy they serialize beautifully to JSON! Not only are most of the internals useless or unreadable to most, but they tend to get pretty long, too. Combined with GCP, these logs will get split into multiple logs and fail to parse, so no one can search for them.
Log as an error. Info severity is logged in blue, warnings are orange, and stderr or error severity is red. Blue is good, and red is bad. But we both know that some errors are fine, right? It was this thing you added earlier and left it there just in case so you know about it. Keep it that way. People should get used to some level of redness in logs.
Log before you rethrow. If you catch an error, you want to know about it, that’s for sure. The best way to know about something happening to your app is to log it as an error. Embrace this pattern, so whenever you catch an error, be sure to log it as an error, then throw it again so the caller knows this as well and handles it in their own way. This way, you won’t miss any errors, I assure you.
Test Implementation Details
The more green checkmarks, the better. In an example application with the standard 3-layer architecture of controllers - domain - data, be sure to have tests for not only each piece of code in each layer but also the integration of all three layers. Do not forget to test even the smallest helper functions. This gives you more green checkmarks. Yes, these tests need to be updated even after a tiny refactoring, but that’s just the tough life of a programmer, and there is no other way.
To help you better understand, change your mindset to “Test the code, not the requirements”. Repeat this every time you struggle.
Use Hardcoded Values in Tests
Tests should be easy to read, clearly state the intent and expectations, and be deterministic. To comply with all these requirements in your integration tests, you can ignore any auto-increments your database makes for new records. Why? The testing environment is deterministic and is set up that way. And if not, it should be. Do not hesitate to make expectations based on this fact.
In this comprehensive blog post, we've covered a range of invaluable development tips that can ruin your codebase, decrease the performance of your Node.js application or just make it a nightmare for the other developers to work with. In fact, it would be ideal to avoid doing any of these things to deliver a high-quality product. Cheers!