As adoption continues to rise, more and more developers will climb the Node.js learning curve, confronting similar problems and coding similar functionalities. Thankfully, the Node.js community has come to the rescue with frameworks and design patterns that not only solve common problems, but also help in structuring applications.
Frameworks generally implement MV patterns like MVC (model-view-controller), MVVM (model-view-viewmodel), MVP (model-view-presenter), or just MV. They also tell you where the code for models, views, and controllers should be, where your routes should be, and where you should add your configurations. Many young developers and Node.js enthusiasts do not really understand how design patterns or OOP (Object Oriented Programming) diagrams map to the lines or structure of the code in their application.
That’s where Node.js frameworks like Express.js and Sails.js come in. These and many others are available to help kickstart the development of web applications. Regardless of the framework you use, you will want to keep certain considerations in mind when structuring your app.
Here are the seven key points I contemplate before mapping out a Node.js application.
1. The right directory structure for the app
While deciding on the directory structure for your app, you should consider the design pattern you chose. This will help with onboarding, finding code, and isolating issues more quickly. I personally prefer using an MVC pattern while architecting a Node.js app. It helps me develop faster, provides flexibility to create multiple views for the same data, and allows asynchronous communication and isolation between MVC components, to name a few.
I like to follow the directory structure shown above, which is based on a combination of Ruby on Rails and Express.js.
2. Mapping ER diagrams to models
As defined in Techopedia, “An entity-relationship diagram (ERD) is a data modeling technique that graphically illustrates an information system’s entities and the relationships between those entities.” An ER diagram outlines the various entities that will participate in our system and defines all interactions between them such that:
- Anything that is an abstract or physical “thing” becomes an entity in a model
- A model maps to a table inside our database
- An attribute or property of an entity translates to an attribute of a model, which is in turn a column inside a table
For example, if your entity is a user, then the corresponding model would be a “User” with attributes such as first_name, last_name, and address inside the database as well as a corresponding table and columns.
Using a simple data architecture makes it pretty straightforward to track your database and file growth any time a new schema is created.
3. Using the MVP pattern
Implementing MVC does not mean just creating folders for controllers, views, and models. You also need to divide your code and logic according to MVC. The code inside your models should be strictly limited to database schema definitions. Developers generally forget that the models will also have code that will perform CRUD operations. Also, any function or operation that is specific to that model should be present inside this file. Most of the business logic related to a model should be in this file.
A common mistake is dumping all of the business logic into controllers. Controllers should only invoke functions from models or other components, transfer data between components, and control the flow of the request, whereas the view folder should only have code to convert objects into human readable form. No logic like formatting data or sorting or filtration should be done inside the view. Keeping the views clean will not only provide a better user experience, but also help you change views without altering any other component.
4. Breaking out logic into modules
As developers, we are always told that we should organize code into files and modules. This does not mean we should try to fit the entire app inside one single file. Dividing your code based on logic and functionality is the best approach. Grouping functions related to a single entity or object into a single file and organizing the directory structure based on logic has many advantages. First, it will save a lot of time determining which function to touch when a bug has to be fixed. Second, it helps to decouple all of the components in the architecture, facilitating the replacement of discrete functionality without the need to modify any other lines of code. Third, it will also aid in writing test cases.
5. The importance of test cases
It’s very important to never cut corners when building test cases—tests are the guardians of your code base. As your application grows, it becomes harder to remember all of the scenarios you must cover while you are coding. Test cases help you keep your code base stable. Testing prevents regression, saving valuable development time and effort. It helps you ensure new features will be pushed error-free. It also helps improve code quality by catching bugs before they go to production. And most importantly, testing helps to instill confidence that the code will not crash.
6. The importance of logs
Logs are useful to debugging and understanding the state of your application. They provide valuable insights into the behavior of the app. Here’s a quick list of things to keep in mind when leveraging logs:
- Find the right balance when it comes to logging. Having “too much information” is never bad, but over-logging will only make your job harder. Needles are easier to find in smaller haystacks. On the flip side, under-logging will result in too little information available to debug or diagnose.
- Split your offline and online logs, wherein the most recent logs are kept for quick retrieval and processing whereas the older logs are archived or dumped to files.
- Consider the frequency and duration of your logs as it will impact the amount of storage you’ll need. Most times the amount of storage you need and the number of logs you have are directly proportional.
And remember, don’t log sensitive data such as email IDs, passwords, credit card information, and phone numbers. It’s not only a huge security risk, but often illegal.
7. Will the application scale?
The worst approach to application development is to think about how to scale after you gain traffic. Instead you should build an architecture that has the ability to grow from the beginning to save time and boost productivity.
Spinning up servers is not scaling; distributing load across resources is. This does not mean that you shouldn’t spawn new servers when the load increases. First, you should set up load balancing within your current resources to handle the increased load. When the load balancing can’t efficiently manage the workload, it’s time to begin horizontal scaling and spawn new servers. You can achieve this through an independent stateless process or via modules. Each process or module will work in an isolated, independent manner. This will not only help your application scale efficiently, but also make your system fault tolerant and easy to recover.
How you structure a web application is as important as selecting the right technology. If the foundations are flawed, the application will eventually crash, or refuse to scale, or in some cases fail to start at all. Never rush into developing new features or new ideas without proper planning and architecture. Bad structure or architecture is like a ticking time bomb waiting to explode.