This post in one of part in my series about Building real APIs with NodeJS for beginners.
All main contents in this series in case we want to navigate quickly:
- From analyzing business domain to designing database models.
- Setup Node project from scratch.
- Using Sequelize & MySQL Docker image for development.
- Organize project structure.
- API Authentication with Passport.
- Writing Unit test for API NodeJS by Jest framework.
- Dockerize NodeJS application.
All these codes in this project can be found here.
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
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 themutils
/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.
time-log-project
-- app.js
|-- package.json
|-- config
|-- controllers
|-- middleware
|-- migrations
|-- models
|-- routes
|-- v1.js
|-- seeders
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
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
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:
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
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. You can download it here and install as normal software.
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.
Don’t hesitate to clap if you considered this a worthwhile read!
~ Happy coding with Mr. Leo ~