Editor’s note: This Node.js project structure guide was last updated on 26 February 2021.
A good start is half the battle, said someone wiser than me. And I can’t think of any quote that would better describe the situation every developer encounters when starting a new project. Laying out a project structure in a practical way is one of the hardest parts of the development process and, indeed, a delicate process.
In this tutorial, we’ll focus on Node.js project structure. We’ll cover the basics of application architecture in general and share some project structure best practices to help you organize your Node.js apps.
We’ll cover the following:
- Why project architecture is important
- Best practices for Node.js project structure
- Create a folder structure for your project
- Separate business logic and API routes
- Use a service layer
- Use a config folder to organize configuration files
- Establish a scripts folder for long npm scripts
- Use dependency injection
- Conduct unit testing
- Use another layer for third-party services calls
- Use a linter
- Use a style guide
- Comment your code
- Keep an eye on your file sizes
- Use gzip compression
- Use promises
- Use promises’ error handling support
Why project architecture is important
Having a good starting point when it comes to our project architecture is vital for the life of the project itself and how you will be able to tackle changing needs in the future. A bad, messy project architecture often leads to:
- Unreadable and messy code, which prolongs the development process and makes the product itself harder to test
- Useless repetition, which makes code harder to maintain and manage
- Difficulty implementing new features without messing up existing code
The main objective of any Node.js project structure is to help you:
- Write clean and readable code
- Write reusable pieces of code across our application
- Avoid repetition
- Add new features without disrupting existing code
Best practices for Node.js project structure
Now we can discuss what I usually refer to as the application structure flow, a set of rules and common practices to help improve the Node.js developer experience.
The Node.js project architecture best practices outlined below can serve as a cheat sheet to help you establish the perfect architecture flow for your next project.
1. Create a folder structure for your project
Everything has to have its place in our application, and a folder is the perfect place to group common elements. In particular, we want to define a very important separation, which brings us to our next rule.
2. Separate business logic and API routes
Frameworks like Express.js are amazing. They provide us with incredible features for managing requests, views, and routes. With such support, it might be tempting for us to put our business logic into our API routes. But this will quickly make them into giant, monolithic blocks that will reveal themselves to be unmanageable, hard to read, and prone to decomposition.
Please also don’t forget about how the testability of our application will decrease, with consequently longer development times. At this point, you might be wondering, “How do we solve this problem, then? Where can I put my business logic in a clear and intelligent way?” The answer is revealed in rule number #3.
3. Use a service layer
This is the place where all our business logic should live. It’s basically a collection of classes, each with its methods, that will be implementing our app’s core logic. The only part you should ignore in this layer is the one that accesses the database; that should be managed by the data access layer.
Now that we’ve defined these three initial rules, we can graphically represent the result like this:
And the subsequent folder structure sending us back to rule #1 can then become:
By looking at this last image, we can also establish two other rules when thinking about our structure.
4. Use a config folder to organize configuration files
5. Establish a scripts folder for long npm scripts
6. Use dependency injection
Node.js is literally packed with amazing features and tools to make our lives easier. However, as we know, working with dependencies can be quite troublesome most of the time due to problems that can arise with testability and code manageability.
There is a solution for that, and it’s called dependency injection.
Dependency injection is a software design pattern in which one or more dependencies (or services) are injected, or passed by reference, into a dependent object.
By using dependency injection inside our Node.js applications, you can:
- Streamline the unit testing process, passing dependencies directly to the modules you would like to use instead of hardcoding them
- Avoid useless modules coupling, making maintenance much easier
- Accelerate your git flow. After you define your interfaces, they will stay like that, so you can avoid any merge conflicts
This is simple but still not very flexible as an approach to our code. What happens if we want to alter this test to use an example database? We should alter our code to adapt it to this new need. Why not pass the database directly as a dependency instead?
7. Conduct unit testing
Now that we know we have got dependency injection under our belt, we can also implement unit testing for our project. Testing is an incredibly important stage in developing our applications. The whole flow of the project — not just the final result — depends on it since buggy code would slow down the development process and cause other problems.
A common way to test our applications is to test them by units, the goal of which is to isolate a section of code and verify its correctness. When it comes to procedural programming, a unit may be an individual function or procedure. This process is usually performed by the developers who write the code.
Benefits of this approach include:
Improved code quality
Unit testing improves the quality of your code, helping you to identify problems you might have missed before the code goes on to other stages of development. It will expose the edge cases and makes you write better overall code
Bugs are found earlier
Issues here are found at a very early stage. Since the tests are going to be performed by the developer who wrote the code, bugs will be found earlier, and you will be able to avoid the extremely time-consuming process of debugging
Fewer flaws in the application means less time spent debugging it, and less time spent debugging it means less money spent on the project. Time here is an especially critical factor since this precious unit can now be allocated to develop new features for our product
8. Use another layer for third-party services calls
Often, in our application, we may want to call a third-party service to retrieve certain data or perform some operations. And still, very often, if we don’t separate this call into another specific layer, we might run into an out-of-control piece of code that has become too big to manage.
A common way to solve this problem is to use the pub/sub pattern. This mechanism is a messaging pattern where we have entities sending messages called publishers, and entities receiving them called subscribers.
Publishers won’t program the messages to be sent directly to specific receivers. Instead, they will categorize published messages into specific classes without knowledge of which subscribers, if any, may be dealing with them.
In a similar way, the subscribers will express interest in dealing with one or more classes and only receive messages that are of interest to them — all without knowledge of which publishers are out there.
The publish-subscribe model enables event-driven architectures and asynchronous parallel processing while improving performance, reliability, and scalability.
9. Use a linter
This simple tool will help you to perform a faster and overall better development process, helping you to keep an eye on small errors while keeping the entire application code uniform.
10. Use a style guide
Still thinking about how to properly format your code in a consistent way? Why not adapt one of the amazing style guides that Google or Airbnb have provided to us? Reading code will become incredibly easier, and you won’t get frustrated trying to understand how to correctly position that curly brace.
11. Comment your code
Writing a difficult piece of code where it’s difficult to understand what you are doing and, most of all, why? Never forget to comment it. This will become extremely useful for your fellow developers and to your future self, all of whom will be wondering why exactly you did something six months after you first wrote it.
12. Keep an eye on your file sizes
Files that are too long are extremely hard to manage and maintain. Always keep an eye on your file length, and if they become too long, try to split them into modules packed in a folder as files that are related together.
13. Use gzip compression
The server can use gzip compression to reduce file sizes before sending them to a web browser. This will reduce latency and lag.
14. Use promises
Promises bring in more pros than cons by making our code easier to read and test while still providing functional programming semantics together with a better error-handling platform.
15. Use promises’ error handling support
Finding yourself in a situation where you have an unexpected error or behavior in your app is not at all pleasant, I can guarantee. Errors are impossible to avoid when writing our code. That’s simply part of being human.
Dealing with them is our responsibility, and we should always not only use promises in our applications, but also make use of their error handling support provided by the catch keyword.
Creating a Node.js application can be challenging. I hope this set of rules helped you put yourself in the right direction when establishing what type of architecture you are going to use and what practices are going to support that architecture.
200’s only Monitor failed and slow network requests in productionDeploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.