Organize project structure (Part 4)

Dec 12, 2018

In this section, I won’t go into details of the logic code too much. You can find all these code in projects repository. Instead, I’ll show the way how we can set up a good project structure. Every component of our project has each own functionality. In the beginning, if we organize it good enough, in the future, we just add code or component to the right place where it should belong to.

application workflow
application workflow

Application Workflow

It will be a good start if we have a chance to review our application workflow to this moment.

  • middleware: stands right after our server receives client’s request. This is a place we can pre-process our request or response before forwarding it to the next phase.
  • routes: it looks like reception guys in our application. It will decide where is the next stop for client’s request.
  • controllers: yes, we have many controllers here. Each controller handles different request.
  • models: are our models in our application that we created in the previous post.
  • services: we can call them utils / helpers or whatever. It contains useful code can be imported in several places. Most of the time, we’ll handle complex logic here and return the result to our controllers.
  • Sequelize ORM: of course, always stays between our models and database. And in case client’s request need to interact with our database to update or get data, Sequelize will help.
  • Whenever client’s request is completed in our server, one response will be returned to our client. That’s the end of one request/response lifecycle.

Project Structure

We’ll add some folders/files to our project that’s quite the same as the workflow we mentioned above.

Screen Shot 2019 06 26 at 6 29 28 AM
project structure

  • controllers: includes controller files.
  • middleware: include custom middleware files.
  • routes: when building an API application, we usually have several API version, so in this case, we’ll split routes to multiple version as well. Let’s start with version 1 first.

Routes

Let’s take a look a little bit into our routes/v1.js file

const express = require('express');
const router = express.Router();
const customMiddleware = require('../middleware/custom');
const UserController = require('../controllers/user.controller');
const TaskController = require('../controllers/task.controller');
const TimeLogController = require('../controllers/timelog.controller');
const ProjectController = require('../controllers/project.controller');
const ReportController = require('../controllers/report.controller');
const AuthController = require('../controllers/auth.controller');
/* eslint-disable */
router.get(   '/users', UserController.getAll);
router.post(  '/users', UserController.create);
router.get(   '/tasks', TaskController.getAll);
router.post(  '/tasks', TaskController.create);
router.get(   '/tasks/:taskId', customMiddleware.task, TaskController.get);
router.delete('/tasks/:taskId', customMiddleware.task, TaskController.delete);
router.put(   '/tasks/:taskId', customMiddleware.task, TaskController.update);
router.get(   '/timeLogs', TimeLogController.getAll);
router.post(  '/timeLogs', TimeLogController.create);
router.get(   '/timeLogs/:timeLogId', customMiddleware.timeLog, TimeLogController.get);
router.delete('/timeLogs/:timeLogId', customMiddleware.timeLog, TimeLogController.delete);
router.put(   '/timeLogs/:timeLogId', customMiddleware.timeLog, TimeLogController.update);
router.get(   '/projects', ProjectController.getAll);
router.post(  '/reports/tasks', ReportController.getTasks);
/* eslint-enable */
module.exports = router;

We’re based on Router support from express framework. The router support variables method: get, post, put, delete, etc… We need to pass path for each router method, the following params usually is a controller’s method will handle the corresponding request. For example:

router.get(   '/users', UserController.getAll);

The request /users will be handled in getAll method of UserController.

In case you want to pre-process request before it goes to controller, we can add middleware as the second params like this.

router.get(   '/timeLogs/:timeLogId', customMiddleware.timeLog, TimeLogController.get);

Now we need to modify our app.js to import routes.

const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
// This will be our application entry. We'll setup our server here.
const http = require('http');
// Set up express app
const app = express();
const v1 = require('./routes/v1');
...
// Require our routes into the application
app.use('/v1', v1);
...
module.exports = app;

Middleware

middleware
middleware

This place is where we’ll pre-process our request and response before forwarding to the next step. For example, in this case, we’ll create a TimeLog instance that’s based on timeLogId value inside a request. This instance will be appended to the request, and we can get that value in our controller.

Controllers

controllers
controllers

You can imagine is an operation place, its main job is to delegate data flow inside our application and return the final result to our client.

I’m not going to other controllers code details. It’s quite the same with project.controller.js. Please take a look in to /controllers folder in case you want to see the rest of controllers code.

Services

Some services as utils include a group of util functions can be used several places in our application. For example:

services
services

This util service helps to handle success or error response. You can see that this service is used in many places in our code.

Another kind of service is that it can help us to handle complex logic we don’t want to put into our controller. For example in report.controller.js file:

report
report

In this case, we want to get the list of task based on project (projectId) and the created date of task (reportedDate). The projectId value could be one number or an array of number in case user want to fetch task from multiple projects. It isn’t quite simple so we’ll let /services/report.service.js help us.

const Sequelize = require('sequelize');
const isEmpty = require('lodash/isEmpty');
const moment = require('moment');
const { to } = require('await-to-js');
const { Task, Project, User } = require('../models');
const { Op } = Sequelize;
const formatDate = date => moment(date, 'DD/MM/YYYY');
const createdDateQueryBuilder = date => ({
  createdAt: {
    $gt: date.toDate(),
    $lt: date.add(1, 'days').toDate(),
  },
});
const projectIdQueryBuilder = (projectId) => {
  if (!projectId || (Array.isArray(projectId) && isEmpty(projectId))) return {};
  if (Array.isArray(projectId)) {
    return {
      projectId: {
        [Op.or]: projectId,
      },
    };
  }
  return { projectId };
};
module.exports = {
  async getTasks(projectId, reportedDate) {
    let date;
    if (isEmpty(reportedDate)) {
      date = moment();
    } else {
      date = formatDate(reportedDate);
    }
const condition = {
      ...createdDateQueryBuilder(date),
      ...projectIdQueryBuilder(projectId),
    };
return to(Task.findAll(
      {
        where: condition,
        attributes: ['id', 'name', 'point', 'createdAt', 'updatedAt'],
        include: [{
          model: Project,
          attributes: ['id', 'name'],
        }, {
          model: User,
          attributes: ['id', 'name', 'role'],
        }],
      },
    ));
  },
};

In this service, we have to build a quite complex query statement, it depends on what parameters we receive from our client as well.

Testing with Postman

Postman is a great tool from Google that helps us to test our APIs. An example to fetch project resources by GET method:

postman
postman

Here is an example of how you can send a request to pull all projects data from our application API. The /GET method with path is localhost:8000/v1/projects.

Until now we already have all APIs needed for our application and know how to organize a good project. Next step, we’ll helps our application more security by adding an authentication method. See you in the next section.