You developed your shiny web application with Node.js, but the work doesn’t stop there. App maintenance can be just as complicated as the coding process. Problems and malfunctions can lead to discouraging clients from using your app. However, if you handle all the issues properly, there is nothing to worry about.
Improper maintenance of an application can result in issues related to stability or flexibility, often leading to the app’s failure. If the code is not well-written or if developers use outdated tools, the performance can suffer, and users might experience more bugs and app crashes. On top of that, poor-quality code can hamper the app’s scaling capacity and the further development of the application. In the worst case scenario, it might become impossible to introduce new features without rewriting the codebase from scratch. In business terms, you will have to put more resources into technology and prepare for a much longer development process.
On the other hand, efficient maintenance can make your application stable and flexible, so you can easily build new functionalities and improve the existing ones. It also translates into lower costs and faster development.
Node.js doesn’t provide any specific convention for developing the application. Frameworks that use Node.js are mostly unopinionated, meaning that they don’t give you any specific guidelines for the way code should be written. That’s why each application requires an individual approach and, as a result, more experienced programmers who have worked out good processes for developing and maintaining code internally.
Many problems with Node.js stem from the lack of good practices. The open-source community is very active and offers plenty of ways of building applications. Having too many options however, could be a problem for unexperienced developers and could lead to application structure incongruency. The ecosystem is still relatively immature. Implementing well tested design patterns borrowed from other more traditional environments like Java is crucial for later code maintenance.
Node.js is a single-threaded process, which makes scaling a little bit more complicated. Developing more complex applications with CPU/MEM-heavy computations might require dividing it into smaller microservices that handle different operations. When architecting a performant and scalable Node.js application, keep in mind it should be relatively small and stateless. Recent years have brought development in the area in the form of different architectural approaches such as the aforementioned microservices or serverless as well as Node.js threading support (still in the experimental form, but they should be a regular feature in Node v12)
Documentation is crucial to every IT project. It gives you an idea of how the app works. It tells developers what the main components are, how they relate to each other, and what the main purpose of the application is. It provides explanations as to why certain solutions, especially the less obvious ones, were applied. Poor documentation will very likely extend the development time, and make the whole process more difficult. It can expose the application to problems with performance and inhibit the implementation of new features.
The first thing you need to do when you notice that something is not working properly in the application is to run a code review, regardless of the stack you use. Code review will tell you a lot about the quality of the code and the stack, and about the application in general. It should always be the first step in the process of discovering problems and potential solutions. Having conducted a successful code review, you can narrow down the real problem and see whether it lies in the performance, scalability, architecture, or flexibility.
When fixing the issues with your application, you should also specify the expected outcome and your resources: time and money. Once you have a clear idea of what you want to achieve, and how much you can spend on it, it will be much easier for your team to choose the optimal solution.
If the actual problem lies in a monolithic structure of your app, you should extract microservices out of the app so that they can work separately. Each mini-application should serve its own purpose, for example: API, communicating with the database, fetching data from external services, etc. It will significantly streamline scalability, because you will be able to move services around different machines. If one of them needs extra computing power (e.g. video transcoding), you can delegate this as a service to a more powerful machine.
Microservices also help dealing with Conway's law, which paraphrased states that "any piece of software reflects the organizational structure which produced it". Having multiple teams working on a monolithic application might be problematic. Divide your app into microservices and let you teams work in isolated environments. That significantly improves flexibility. It’s especially useful when you hire a new team to implement a change in one microservice. Your dev team doesn’t need to go over the whole structure, just the part that needs an update.
Microservices have some cons that you will need to consider, though. Depending on the type of the application, using microservices might be more problematic than in the case of a monolithic app. It will require more time for devs to get to know the app. They will need to dive into all the repositories, which takes more time when you hire a new team. Microservices can also result in increase complexity when it comes to deployment and testing.
If the application works decently, but the overall code quality seems to be low or inconsistent, you should first introduce some conventions. Clean up the file structure by dividing it into logical units. It’s good to implement static code analysis tools (e.g. Codebeat) and use a type systems (Typescript, Flow). Make sure that the deployment process is driven by a properly configured Continuous Integration. Put some linters into the pipeline to make sure you don’t push poor or inconsistent code to the production.
Make sure you include code refactoring into your daily development process. Performing code optimisation on a regular basis will prevent growth of technical debt and will ensure new great ideas being implemented in the entire codebase. It improves velocity development in a long term perspective and makes developers happier and more productive. This also makes project more readable for newly onboarded dev team members.
If you need to implement or rewrite some functionalities, make sure your app’s behaviour is well tested before you start your work. All crucial parts of the application should be properly tested in advance. Imagine that you need to rewrite a part of user management logic in your application. It will be much easier to implement changes properly, if you are certain that current functionality has been fully tested. In this case, if something stops working after your changes, you’ll know about it immediately.
Good documentation for your application is crucial for the efficient development and good communication with the team. That’s why you should ensure that the documentation is always updated and covers all the general information about the app and the key facts. Good documentation needs to be easily understood and should contain accurate information that helps users get the most out of your software. It should be easily accessible to anyone in the team, and it should explain procedures and problems. If your documentation lacks some important information make sure you update it as soon as possible.
Upgrading Node.js to the latest version is pretty straightforward. It can be updated by devops, unless something is not working well – then you might need the help of some developers. Keeping the Node.js version up-to-date can boost your app’s performance (but doesn’t necessarily have to). It also gives the developers the ability to work with modern native solutions, such as async/await and other ES6/7 features, which can make the development process faster. There will be no need to setup transpilers, you will use fewer third-party libraries, and have a more bulletproof stack.
That said, updating Node.js version is not what you should worry about. It rarely generates backward compatibility problems. Most issues result from using outdated open-source modules. The application can use libraries that are not supported or maintained anymore, which, in Node.js world, will accrue technological debt pretty fast. If you have found any outdated libraries, first of all, check if they’re still actively maintained. If so, prepare a plan for an update. You have to be sure that it won’t break anything. Check whether the app has been tested through and through – a test coverage tool will be helpful here. But what if the library is not maintained at all? Look for alternatives, and if there aren’t any, make sure that your tests have you covered, and you’ll be the first one to know if something stops working.
If the app is not performing very well, you need to check the roots of the problem. See whether it’s an architecture problem, for instance, the machine is too weak, or auto-scaling is not implemented, or whether the problem lies in the code itself or maybe just a part of it. In each case, you need to narrow down the problem first. What you will have to do will depend on the results of your investigation – it may be implementing autoscaling, changing the architecture, or rewriting some parts of the code. A single universal cure doesn’t exist. But there’s one thing you should always have in your belt: a full-featured monitoring service such as NewRelic and Instana. Nothing will help you better in getting to the heart of the problem than logging the application’s behaviour.
In one of our commercial projects, we started from one, monolithic B2C application which extensively used Salesforce API, crons, websockets, and a lot of external service integrations. It was a perfect fit for that time. However, a little bit later, the client decided to focus mainly on B2B. Such shift meant that the business logic of the application became completely different. Still, some of the functionalities such as querying the Salesforce API, DB operations, crons, or event handling remained the same. Therefore, we’ve decided to split the monolithic B2C application into several reusable microservices (e.g. a Salesforce API service, DB service, synchronisation service) that could be leveraged in both B2B and B2C apps.
This gave us much more flexibility when writing the B2B variant of the application. On top of that, we avoided a lot of code duplication, thanks to the sharing of microservices. Right now, any change we make is immediately reflected in both apps. Another advantage is that when a service needs more power, we can scale only that particular services, avoiding incurring unnecessary costs.
Node.js is a great fit for many solutions, but as any other technology, it may cause problems if it isn’t maintained properly. Whenever you encounter a problem, dig into the code, verify the documentation, and discuss the application with an experienced team. The vast majority of issues with Node.js applications can be detected with a code review and solved with a right approach.