Node.js Project Structure Beyond the Basics

Hello there, In this article, we will talk about the basic structure for the Node project. This pattern will help you to make an efficient and scalable solution. It will improve readability thus easy for the Maintainance of the project.

We will be using Javascript. I mean not Typescript. If you are interested in Typescript and looking for better architecture I would suggest you look at Nest JS, If interested let us know we will write an article on NestJS as well.

Introduction

While learning most of the suggested project structures structure is Model, View and Controller model. There is nothing wrong with that structure. Actually, that is the best structure out there which makes the backend learning fun. It will be perfect for some projects, but with the addition of the features and codes to the project, it will be hard to keep track of the project.

Here we will introduce a service layer which will interact with the database. In the controller, we will do all the data processing and once everything looks good we will pass it to the service to save/update the data.


I will share the git repository, so, not to worry much about recreating this structure. Just try to understand how we control the follow of the data and will help on improving the project.

Project Structure

We will have app.js, which will be a basic express app. It will have all the express kinds of stuff and will return the app (express()). By doing so, we don't have to worry about serving the app and database connections stuffs here.

Next, we will have server.js, which will import the app and database connections. we will create the database connection. and once the database connection is established, we will run the express app (ie. start listening on the port).

We will be using the dotenv npm, which will read from the .env file and set the process.env. Thus any configuration variables will be stored on the .env file. We won't be sharing the .env file through the version control system, to make it easy for other developers to understand the .env file's structure. we will have a .env.save.

The Directories

We will have different directors with their own dedicated tasks.

Controllers

The controller is the one responsible for handling the user request. The controller will get the user request, and it will get the necessary data to analyse the request, request for necessary data from the Database through the service layer and pass the result back to the user.

For example, a user controller has different controller methods, among them createUser. createUser is responsible for creating the user, but before that, it needs to validate the provided data are correct, and that there is no conflict in the data (multiple users with the same email). Once it is verified, it passes the data to the Service layer to save it in the database. It will get back the user object, which will then pass back to the users with the appropriate status code.

db

Will contains the database connection related files. Here we have a database connection to Mongo DB.

Middleware

middleware are the functions which are called before the controller methods are called, In the middleware, we can have different logic, which will perform different actions based on the data from the request.

For example, we have userAuth middleware, which will get the token (either for header or query) to verify the token, if the token is valid save the user's information on the request and call the next method. Elise if the token is not valid it will throw an unauthorized error.

Models

Models are the representation of the collection on the database. In the model, we will have the collection on the database and the data structure that is to be saved on the collection.

Along with the database model, we will have DTOs there. DTO will do the validation of the data.

Routes

Routes will have all the files related to routing the requests. Routes will define a route, add any middleware in the route (if required) and provide the associated controller function to be called when the user requests that endpoint.

Services

Service is the layer which is used by a controller to interact with the database. Service provides an easy to use API (functions) to access the database.

As discussed in the controller section, the Controller does all the validation and verification. Service just saves, updates and retrieves the required data.

Utils

In the utils directory, you can have those files that cannot be on any of the above directories. If you have some similar utils, you can give a dedicated directory for them as well.

Final Thoughts

I love the way that, while working on different parts of the code, I don't really need to care about the other issues. In this structure, while working on the Service part, I don't have to worry about the data is invalid or will cause issues with data consistency. I just have to worry about the way to save and update and retrieve the requested data. That's all.

Similarly while working on the controller, We can validate the data with the DTO. After that, I don't have to worry about invalid data. As it will already be filtered out. Then in the controller, we request all the data needed and prepare the data to be updated and pass it to the service.

In the Database model, we can define all the database consistency. As well as on the DTOs, we will add all necessary validation without worrying about how we gonna handle the validation process on the controller.

It feels like performing different roles every time working on a different section of the code.

Code Snippet

let's have a look at some code snippets as well. 

Here is the user controller,

We first validate the data with joi and then create the user. Additionally, we can add another check if the user with that email exists and throw a 409 conflict error to the client.


const UserDTO = require("../../models/user/user-dto");

const UserService = require("../../services/user");

const postCreateUser = async (req, res, next) => {

let value = UserDTO.createUserDTO.validate(req.body, {
abortEarly: false
})

if (value.error) {
return res.status(422).json({
message: "Validation Error",
error: value.error
});
}

value = value.value;

// TODO make sure that you hash the password before saving it to the DB
// let hashedPass = someHashFunction(value.password);
// value.password = hashedPass

let user = await UserService.createUser(value);

res.status(200).json({
message: "User Created",
user: user
})
}

const userController = {
postCreateUser,
};

module.exports = userController

Here is the user Service,

we simply save the passed data.


const User = require("../../models/user");

const createUser = (createUserDTO) => {
let aUser = new User(createUserDTO);
return aUser.save();
}

const UserService = {
createUser,
}

module.exports = UserService;

the user model,

Just a simple mongoose model.


const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const userSchema = new Schema({
fullName: {
type: String
},
email: {
type: String,
unique: true,
index: true
},
password: {
type: String
}
})

const User = mongoose.model('user', userSchema);

module.exports = User;

The user DTO,

just simple plain validation schema.


const Joi = require('joi');

const createUserDTO = Joi.object({
fullName: Joi.string().min(5).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
})

const UserDTO = {
createUserDTO
};

module.exports = UserDTO;

The user routes,

this user route is used as app.use("/user", userRoutes); Have a look more on the repo.


const express = require('express');

const userController = require('../../controllers/user');

const router = express.Router();

router.post('/', userController.postCreateUser);

module.exports = router;

Next Step

What could we do to improve this? Well, there may be lots of ways we can improve the structure. 

I will keep updating the repo, thus you may get different on the latest commit, I would suggest checking the commit logs and exploring the development process.

The next steps may be to add some comments and documentation. we can use jsdoc to swagger to add swagger API documentation.

Adding jsdoc comments to methods and describing them.

Prepare stander docs for API structures and patterns and git-flow as well.

Conclusion

In this article, we look through a node project structure. It may not be the best solution that suits you, but It's really good for efficient development, readability, and project maintenance. This structure is kinda inspired by the NestJs structure. If you want the best structure for your project I would suggest having a look at NestJs.

This structure is I believe a level improvement on our good old MVC pattern structure.

You can find the code used in this article on this GitHub repo.

Posted by Sagar Devkota

1 Comments

  1. I found this structure quite clean and easy to maintain. Thanks for sharing.

    ReplyDelete