MoSwilam
MoSwilam

Reputation: 944

NodeJs MVC architecture, is this macaroni code?

I am a junior NodeJS dev, currently working on a cryptocurrency exchange platform. here is the project folder structure.

/app
    /controllers
        /user
        /order
        /settings
        ...
        index.js
    /middlewares
    /models
    /routes
        user.js
        order.js
    /services
        user.js
        order.js
        ...
    /views
    index.js
/config
/migrations
/public
/utils
server.js
.env
...

Now, at first, it was a bit overwhelming, but later on, I became comfortable moving around the app.
except for one particular file! the index.js in the controllers directory, here is how it is set up.

const { readdirSync, lstatSync } = require('fs');
const { validationSchema } = require('../../utils/validator');

module.exports = readdirSync('app/controllers')
  .filter(name => lstatSync(`app/controllers/${name}`).isDirectory())
  .reduce((controllersAccumulator, dir) => Object.assign(
    controllersAccumulator,
    {
      [`${dir}Controller`]: readdirSync(`app/controllers/${dir}`)
        .map(fileName => require(`./${dir}/${fileName}`))
        .reduce((accum, controllerFile) => Object.assign(
          accum,
          Object.keys(controllerFile).reduce(validationSchema.bind(null, dir, controllerFile), {}),
        ), {}),
    },
  ), {});

I have to admit, this has been always scary to me, just to look at it! so what it does in simple words is, it maps the routes requests to the handlers in the controllers directory.

so for example, If a user wants to make a post request to register a new account: the route path will be like so:

// in the routes dir, user.js 
const { userCOntroller } = require('../controllers/');

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


// in the /controllers, then /user, there will be a registration.js that includes:
const UserService = require('../../services/user');

exports.registration = async (req, res) => await UserService.create(req.body);


//then in the /services directory, user.js
...
class UserService {
  static create(body) { ... }
  ...
}

module.exports = UserService

so what I am still unable to understand is, how did we come about to have the userController which is imported in the user routes in the last snippet? so this is what the index.js file in the /controllers has produced!
when I asked the senior guys in the team, they said, yes it's hard to read but its less code. well, ok :\

so, what could have been done differently to make this file more readable, in other words, is there a way to refactor it? thanks in advance!

Upvotes: 1

Views: 71

Answers (1)

Tomasz Kasperczyk
Tomasz Kasperczyk

Reputation: 2093

The easiest way to explain the first code snippet would be to rewrite it and add appropriate comments:

//Get all file names inside the controllers directory
const fileList = readdirSync('app/controllers');
//Get only directories from the file list
const onlyDirectories = fileList.filter(name => lstatSync(`app/controllers/${name}`).isDirectory());
//Create an empty object that will be exported from this module. Properties of this object will be assigned in the "for of" loop
const objectToExport = {};
//Import all files from the given directory and assign them to objectToExport
for (let directoryName of onlyDirectories){
    //First get all file names
    const controllerFiles = readdirSync(`app/controllers/${directoryName}`);
    //Then require each of these files
    const controllerModules = controllerFiles.map(fileName => require(`./${directoryName}/${fileName}`));
    //Assign the imported modules to `objectToExport`
    controllerModules.forEach((controllerModule) => {
        //Validate each module and assign it only if it passes validation
        if (validationSchema(directoryName, controllerModule)){
            objectToExport[`${directoryName}Controller`] = controllerModule;
        }
    });
}

module.exports = objectToExport;

Below I'll address your follow-up questions from the comments. The resulting object now looks like this:

{
    userController: {
        registration: [Function],
        ...
    },
    orderController: {
        ...
    },
    ...
}

The registration function ended up in the userController property because it was exported by registration.js, which was then imported by the require statement in my first code snippet. In order to use this function directly in other files, you have to destructure it in the following way:

const { userController: {registration} } = require('../controllers/');
//now you can use it like this:
router.post('/registration', registration);

Your last question is about req.body. As you can see, the function takes two parameters: req and res:

exports.registration = async (req, res) => await UserService.create(req.body);

It is then passed to your router as a middleware. I'm assuming you're using Express.js as your framework. If so, req and res are passed automatically by the router to the registration function. That's how they can be used by UserService inside this function. The body property is created automatically by Express as described here.

Upvotes: 1

Related Questions