Antipatterns That Lead Your (not only) Node.js Project Nowhere
Node.js is very popular right now when it comes to software development. Many companies, from startups to big tech, use it to create their services. Node.js is very performant for many kinds of I/O-heavy services, and there are plenty of choices, when it comes to libraries and tools, thanks to its large open source community. This runtime gives us freedom: you can use it with JavaScript or TypeScript, write classes, functions, or a mixture of them, use more opinionated frameworks or unopinionated ones, and you're not locked into any specific paradigm that defines your choices. That's great, but at the same time it is a dangerous environment that lets you create spaghetti code that will push your project into the void and cause the business to lose money. Sometimes when I read articles or watch some videos, I see people who promote the easy path, cutting corners, talking about classes vs. functions, which framework you should use anyway, elaborating on new functions in frameworks, etc. When you go and work on a real project at scale, where you need robust solutions, clear separation of concerns, and well-tested, maintainable code that will assure you that your application will be reliable and won't put the business at risk, the landscape is totally different. It turns out that fancy frameworks and state-of-the-art libraries are not always the best choice, and you reach for more opinionated patterns you saw elsewhere. In this article I'd like to talk more about things you can screw up in your (not only) Node.js project.
Lack of decent tests (and paying attention to them)
I think this is one of the most, if not the most, critical antipatterns not only in the Node.js land but in the entire industry. Neglecting tests can lead a project into serious problems, including legal ones. Not only avoiding tests but also having poor-quality tests are serious risks. Not setting rules and a good testing strategy is a big antipattern. When a project runs on real user data, you need to have unit, integration, and end-to-end tests in place. When an application expects high traffic, you need to have load tests as well; there is nothing more stressful for developers than deploying code without knowing how it behaves in a high-traffic scenario. Having tests is not only about 'test coverage' but also about test quality. It is important to set the rules and testing architecture, defining what a unit, integration, and end-to-end test mean in your project. For me, when it comes to the approach to testing, the TDD approach is a gold standard everywhere you need stable, reliable, and safe code; in some industries such as fintech, healthcare, public transport, and air transport, this approach is mandatory. Of course there are plenty of people who would say that it is slow, or that time to market is more important, but in real life, sooner or later, this rapid approach will lead to money loss for customers, leaked private data, or legal troubles, and you could be forced to take responsibility for this.
Lack of code review
Another sin in delivering software. Delivering in a hurry and squeezing developers' time down to only producing code is broken. Setting up a team where people do not know what is actually being delivered outside their sandboxes is very dangerous. If you feel guilty as you spend your time traversing a codebase to gain even a grain of knowledge about the code, your environment is broken. If we work in a structure called a team, WE deliver code, not he or she or me; the task does not finish when code 'works' but when everyone who works with this code agrees to the changes. This guarantees that everyone has a clear view of the whole picture, not just part of it.
Lack of well-established patterns in code and paying attention to them
How to write a solid code review in a project where there are no shared coding conventions? One developer likes function declarations, another prefers using function expressions, everyone has their own preferences when it comes to code; when you create your own solo project you probably care about consistency, you know your style, and you stick with it. The same applies to real production code: consistency is mandatory and you have to set the rules to keep code consistent. Together with the team, make agreements to deliver the best quality code as a result of the whole team's experience and preferences. To make it easy to apply in practice, use linters and commit hooks; this will not only make code quality better but also allow developers to focus on the things that matter in code review, instead of repeatedly correcting the same inconsistencies.
Lack of communication between developers
Asynchronous work is a great experience, allowing developers to focus on hard problems and pursue deep work, but when there is no habit of synchronous meetings, where the team can discuss issues and learn from each other, the team becomes a group of independent freelancers contributing to the same project, solving the problems they encounter alone and approaching them as they want. There is no robust code review, no decent code quality, and no reliability when the team exists only on paper. Pair programming sessions can also be very useful to unblock people and work on things truly together; remember, when you work together you have at least two brains, so it is safer, faster, and better. I am leaning toward the view that the team should have shared intelligence, a shared brain, when it comes to your code.
Weak or missing application architecture
You do not always need a dedicated architect to think seriously about architecture, you just need to learn something more about the big picture, not only about the frameworks and libraries being used. Sometimes there is no architecture in the project, and sadly it is often derived from the lack of communication in the team and squeezing developers' time down to only delivering new lines of code. I know there are pivots, changing requirements, and so on and so forth; code is for business, for solving problems, that's right, but at the same time code is the backbone of the actual product, and we cannot neglect architecture. Weak architecture usually leads to more technical debt over time. Nobody knows where to put a new function or class, or where the business logic should sit, because it is spread across all the layers of the app. Architecting a project is not about creating abstractions or annoying rules that you need to learn, but about speeding up the overall process of delivering a project, from development to maintenance. Node.js is about freedom: you can do whatever you want. It is just a runtime that provides some functions and allows you to run JavaScript, but if you let things be created without control, I can assure you they will be unmaintainable from the start.
Blindly accepting generated/copy-pasted code
It has always been the same story using Stack Overflow or now AI. You need to quickly scaffold something, or you don't know something, so you reach for AI and you have an answer almost immediately. Sometimes the amount of code produced by AI is big, and it is really hard to focus when reviewing changes, so you push code that you think you understood, but it is only an illusion. The same applies to copy-pasted code from another part of the codebase or from Stack Overflow or any other source, still the key is full understanding of the code you and your team produce. To make it right, teach your team to make sure they understand the code that is delivered, ask for review, and ask for an explanation when not everything is clear.
Using TypeScript the wrong way
These days TypeScript is mandatory when building a real product at scale with a team of developers. TypeScript catches bugs early, can document code very well through types, has a great LSP that allows your editor to show type messages and type hints perfectly, and since it was rewritten in Go it is so fast in day-to-day development, but... TypeScript can mess up your code and pollute your logic terribly if you use it wrong. If the app requirements change and the data model is not consistent with your types, it will sooner or later slow you down. This tool should align with your intentions and decisions, reflecting them in the code. Do not neglect updating your interfaces and types, or even refactoring if needed; remember about validation, TypeScript is gone at runtime, and always use it in strict mode, forcing your team to cope with the rules.
Wrap up
Maybe I should have put it as a distinct section in this article, but many things I touched on here are, not always but often, caused by hurry. You can think about tests, architecture, clean code, you care about communication, but you feel deadlines breathing down your neck, and you cut corners as you worry about the project, your position, etc. But there is no other way around than being frank about the complexity of the task, communicating blockers, demanding meetings about architecture and code patterns, sparking discussions, disagreeing when someone breaks the contract with the team, and asking questions when someone provides you with obscure requirements. Despite trends and fancy slogans that code is cheap and easy to create, code is a responsibility and the foundation of a good project, and you and your team are the people who deliver it and care about it.
