Most technical tutorials teach you the toolbox of a language. Still, people neglect the importance of a stable project structure when entering a new project or starting a new job. Of course, learning the foundations of a programming language is the first central stepping stone for every programmer. But becoming a highly skilled developer can be much more accessible by learning to set up your projects correctly. Node.js, for example, may seem easy for configuring or setting up a “playground” workspace. However, it can become tricky when it comes to complex backend project designs and building APIs.
So today, I’ll focus on what makes an excellent Node.js project, based on my own experience as a software engineer. I will also go into more detail about its most crucial components and share tips on keeping your project well organized. Be aware that this project structure guide is suitable when writing Vanilla Javascript.
Folders first
Speaking of a good project structure, a question in every developer’s mind is, “Where should I put this file?”. Creating the folders where you will store your source code is the first thing to consider when beginning a project. To emphasize their importance, therefore, I will refer to the folders of the Node.js project as “project components” from here on.
Remember that you can have multiple domains if you are building a complex back-end. In that case, your root project folder would most likely have a package entry point. Usually, it consists of separate applications (APIs) that can communicate with one another. Every component listed below forms a project component structure that would ideally apply to all the packages in your project.
1. Every component has its index
After you have created your project components and you start writing JavaScript code, you will probably make at least two or three files in each component. These files will have functionalities you’ll use around your project, meaning they will be imported (required) multiple times. Typically, we can treat every JavaScript file as a module when using the import/export syntax. Instead of exporting the functionalities from each module separately, you can create a single export endpoint from the component. Most of the time, this is an index.js file. Let me give you an example:
Here we have two models (we will explain what models are in a bit) – contact and a group. There is also the index.js file which exports them to other components without concretization.
That will result in a cleaner syntax when importing since JavaScript automatically detects the index.js file. Thus, you won’t need to specify its name every time you import.
After you know how to export/import from different project components in the project structure, it’s time to talk about the most important components you will need.
2. Node.js project components
We want to end up with packages (if you need more than one) with a project structure similar to the one below. We’ll go through every project component and its purpose.
- Config – the config folder is used to organize configuration files. The most common usage of the config file is to export environment variables to the other parts of the project or some database configurations. This is a must-have file, and you will most likely need to create it and set it up in the beginning of the project. Without it you won’t have a proper place to connect to databases or set up environments (e.g. test, production, etc…).
- Controllers – here we will put the handlers of our routes. As we’ll see in the routes/ component of the Node.js project, when the client “pings” our routes for the different CRUD operations , we create handlers to represent each one of those operations. In the controllers we use the services’ logic to handle responses to the client. A good naming convention is the entity associated to that specific controller followed by controller (statistics-controller.js, contact-controller.js).
- Data/Enum – this is a component designed for implementing constants and enumerables. Instead of using strings for data coming from a third party API, e.g. statuses, you can create constants in the data/ folder which you can reuse easily.
- Middleware – the name speaks clearly – you can have a separate project component for the middlewares that you use in the routes. A good practice is to have subfolders defining the purposes of the middlewares. For instance, you can have auth/ subfolder for all authentication middlewares, store/ subfolder for all the middlewares regarding the store entity, and so on.
- Models – every model is an object matching the fields in the table by name and type. They do not make any changes to the data but simply represent it in your codebase. Depending on the ORM you are using, models will be different but you can store them as separate js files – user.js, group.js, etc.
- Routes – everything related to routing belongs here. Routes are the most essential part of an API, so having them well structured will impact the software development life cycle. Remember the “every file has it’s index” earlier? Here we can make the most out of it by defining more abstraction to your routes, specified in separate project components. That makes it easier and cleaner for everyone to understand which routes relate to which entities. A good naming convention is the name of the entity associated to that specific route followed by routes (contact-routes.js, group-routes.js).
A well-structured index.js in the routes/ folder
We make use of the controllers/ in the separate routes to define CRUD operations, as the example below shows:
- Services the business logic in your application is stored here. These are mostly files exporting classes or a giant function depending on your Node.js project requirements. Essentially, theycontain the more complex functionalities in your codebase. In almost all cases, services and models will relate to one another. For example in services you will use the database tables representation coming from the models as data. This data is the same you implement features in the application with.
The service methods are called in the controllers mentioned earlier. It’s a good practice to create a base or a subfolder that will represent the common logic of different services Also, when you have a group of services sharing the same business logic (for example, coming from a third party API), it would be more useful to place them in a subfolder. The two most common naming conventions when it comes to services here are camel case or placing a “.service” at the end of the file name (e.g. statistics.service.js or statisticsService.js and so on).
- Utils– these are the so-called utility or “pure” functions. You pass them an input and they simply mutate it and return an output. They are called utility because you can use them in multiple parts of your system, regardless of the business logic behind them. Notable examples are date conversion or encryption/ decryption of functions.
- Validators – you use validators mainly to approve payloads and configuration, which is essential for the API’s. When receiving data from the client, you want to make sure it is valid so your services can work with it.
- Tests – after you have created the key components in your folder structure, it’s time to implement testing in your project (both unit and integration). Testing is really important when it comes to developing your applications, and I guarantee that the time and effort you put into it are worth it.
Let’s now dive a bit deeper into the structure of your tests in the project. In most cases, you will deal with both unit and integration tests. Since integration testing covers more components of the system rather than a single unit of code, you can create a tests folder in the root of your source code to hold the tests for the rest of the components. For example, if you have tests for the services, you will create a subdirectory in tests -> services. Basically, you repeat the src structure in the tests.
Also, a good practice when it comes to unit testing is keeping the tests folder as deep in the project structure as possible. Let’s say you want to write unit tests for your contact model shown earlier. If so, you create the unit tests in src/models/tests/Contact.test.js.
3. More than folders
- Use a Linter (eslint) – when you have a dev team following some sort of coding conventions such as styling and naming, or when you want to write clean code without producing a mess, ESLint (a static code analysis tool) is your best friend.
- Environment variables – you store these variables in a so-called “.env” file. As the name implies, they provide information about the environment in which the project is running. For example, you can have credentials for establishing a connection to an API that apply only for you and you do not want to share them remotely on Github. That’s why these files are important since they are always in a .gitignore so they are not shared with anyone remotely.
- Scripts – create a scripts folder for some bash scripts which you can run easily afterwards.
- .editorconfig file – this file will help you unify the editor/IDE experience for all the developers who have access to your Node.js project. Here you can specify end of lines, charset, indent size and many more.
Conclusion
The intent of this article was to give you a basic overview of a typical Node.js project’s initial structure. Throughout it, I’ve shared some best practices, such as naming conventions and project component structure. Hopefully, you can now confidently dive into your next JS project.
In case you still have hesitations about using Node.js as a back-end runtime environment, continue with this article. It covers the main strengths of Node.js in more depth. Stay tuned.