Skip to main content

Express.js Projects

Project Setup

  • RULE #1: always start a new project with a fresh new installation of Express with the latest libraries. If the project is similar to a previous one, still do not clone it! In this second scenario, after creating a new project with everything up to date you can copy and paste single functionalities if needed; it would be better to rethink everything. In this way, you can find useless dependencies, dead code, code smells, and pitfalls..

  • RULE #2: if you plan to start from an old Express project, update all dependencies first. Do not start to work with outdated packages.

  • RULE #3: although Express can be used also for building web application with views, consider to use it only to build fast APIs.

  • RULE #4: Express and a SPA stack best fits with MongoDB; however it integrates well also with relational databases

  • RULE #5: Express doesn't force conventions: everyone can setup a project in a random way. The convention must be defined in a per company way and followed for every project.

Must to have packages

It's easy to start with few packages and finish the project with lot of dependencies. Despite other frameworks like Rails or Django, node packages tend to be less stable during time (that means lot of breaking changes). So the goal is to add only a few libraries and follow these best practices:

  • Do not add packages that are not actively maintained or with a few stars
  • Do not add unknown packages just to reinvent a little wheel: see what they do and create a library.js inside the lib folder
  • Add the minimum number of packages to your project: more dependencies mean more issues in the future if you have to upgrade the project

Below a list of packages that can be always be included:

Project structure and naming conventions

- api/
- v1/
- auth/
- auth.router.js
- auth.controller.js
- auth.model.js
- auth.validator.js
- auth.service.js
- user/
- user.router.js
- user.controller.js
- user.model.js
- user.validator.js
- user.service.js
- libs/
- errors/
- base.error.js
- 404.error.js
- ..
- commons/
- custom.integration.js
- ..
- middlewares/
- passport.middleware.js
- authorizeRequest.middleware.js
- ..
- views/
index.js
routes.js

External routes file loads domain specific routes, in this way:

'use strict'
import passport from './middlewares/passport.middleware'
import authRouter from './api/v1/auth/auth.router'
import userRouter from './api/v1/user/user.router'

export default function routes (app) {
app.use('/api/v1/auth', authRouter)
app.use('/api/v1/users', passport.authenticate('jwt', { session: false }), userRouter)
}

Specific routes define domain specific endpoints:

'use strict'
import userController from './user.controller'
import ROLE from './role.model'
import authorizeRequest from '../../../middlewares/authorizeRequest.middleware'
import asyncHandler from 'Express-async-handler'


export default Express
.Router()
.get('/me', asyncHandler(userController.me)
.put('/me', asyncHandler(userController.updateMe))
.post('/', [
authorizeRequest([ROLE.ADMIN]),
asyncHandler(userController.create)
])

Define controllers as classes:

'use strict'
import UserService from './user.service'
import UserValidator from './user.validator'
import _ from 'lodash'

class Controller {
async me (req, res) {
const me = req.user.toObject()
res.json(me)
}

async updateMe (req, res) {
const userData = _.pick(req.body, ['name', 'surname', 'language'])
const errors = await UserValidator.onUpdateMe(userData)
if (errors) {
return res.status(422).json({
success: false,
errors: errors.details
})
}
const result = await UserService.update(req.user.id, userData)
if (result) {
return res.json(result)
} else {
return res.status(422).json({
success: false,
message: 'Failed to update profile.'
})
}
}
}
export default new Controller()

What a controller does:

  • Picks only the right data from the payload
  • Validates the data
  • If everything is corrected, passes data to the Service
  • Returns the right response to the user

Define Services as classes:

'use strict'
import User from './user.model'

class UsersService {
async update (id, data) {
return User.findOneAndUpdate({ _id: id }, data, { new: true })
}
}
export default new UsersService()

Services don't perform validations or authorizations: when they are called with the right data, they just complete the action.

Service Objects?

A service object is a Javascript class that performs a single action. It encapsulates a process in your domain or business logic.

Express apps tend to start simple, with clean models and controllers. Then you start adding features. Before you know it, your models and controllers are big, unwieldy, and hard to understand. Refactoring into service objects is a great way to split these big pieces up, so they're easier to understand, test, and maintain.

As your application grows, you may begin to see domain/business logic littered across the models and the controller. Such logics do not belong to either the controller or the model, so they make the code difficult to re-use and maintain. A service object is a pattern that can help you separate business logic from controllers and models, enabling the models to be simply data layers and the controller entry point to your API.

We get a lot of benefits when we introduce services to encapsulate business logic, including the following:

  • Lean controller - The controller is only responsible for understanding requests and turning the request params, sessions, and cookies into arguments that are passed into the service object to act. The controller then redirects or renders according to the service response. Even in large applications, controller actions using service objects are usually not more than 10 lines of code.

  • Testable controllers - Since the controllers are lean and serve as collaborators to the service, it becomes really easy to test, as we can only check whether certain methods within the controller are called when a certain action occurs.

  • Ability to test business process in isolation - Services are easy and fast to test since they are small classes that have been separated from their environment. We can easily stub all collaborators and only check whether certain steps are performed within our service.

  • Reusable services - Service objects can be called by controllers, other service objects, queued jobs, etc.

  • Separation between the framework and business domain - Express controllers only see services and interact with the domain object using them. This decrease in coupling makes scalability easier, especially when you want to move from a monolith to a microservice. Your services can easily be extracted and moved to a new service with minimal modification.

The most important thing is to create service objects that ARE NOT DEPENDENT from the request: you must pass them clear parameters but not the entire request object.

The service object must not check authorizations and users, if someone calls it, it performs the business logic without asking why.

Testing

  • RULE #1: Setup automated tests ONLY if you are sure that for all application lifetime you'll maintain them. Otherwise it's time wasted.

  • RULE #2: Setup automated tests ONLY if you have budget and time to keep them in sync: a good codebase costs 30% of project time.

Below some useful libraries to test Express and Javascript applications in general:

WHAT TO TEST

  • RULE #1: Do not test everything, don't lose time testing first (unless you are a really fun). Test what means to be tested, like:

    • difficult calculations
    • complex features
    • service objects behaviour
  • RULE #2: Avoid mocking and stubbing requests because your tests will probably pass also if the external APIs for example has changed their response.

  • RULE #3: Avoid mocking database: create less tests but save data into database: remember to cleanup everything at the end of each test.

  • RULE #4: As per #3, do not generate dependent tests: they should be run randomly and they must pass all the time.

  • RULE #5: forget about code coverage: test what really means to be tested, sometimes fixing the code is less expensive than creating huge test suites

Use import ... from

Generally, import is more preferred over require because it allows the user to choose and load only the pieces of the module that they need. The statement also performs better than require and saves some memory.

Node still has experimental support for ES6 modules. To enable them we need to make some changes to the package.json file: In the package.json file add “type” : “module”. Adding this enables ES6 modules. Now run the index.js file by typing node –experimental-modules index.js in the terminal.

Use strict

Remember to enable strict mode in every js file, in order to avoid future errors and be compliant through all application: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode Note that using import ... from enables strict mode by default.

Style guide & Linting

Consider using JavaScript Standard Style: https://github.com/standard/standard That's the simple to setup and very widely used formatter, linter and style checker on the market.

General best practices: