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:
- axios to perform remote calls: https://github.com/axios/axios
- cors middleware to setup cors: https://github.com/Expressjs/cors
- multer to handle mutipart uploads: https://github.com/Expressjs/multer
- body-parser middleware, to handle requests body: https://github.com/Expressjs/body-parser
- lodash, as general utility library https://lodash.com/
- nodemailer to send emails: https://nodemailer.com/about/
- passport for handling authentication: http://www.passportjs.org/
- passport-jwt to handle JWT authentication: https://github.com/mikenicholson/passport-jwt
- pino as general logger: https://github.com/pinojs/pino
- joy as validator: https://github.com/sideway/joi
- mongoose if you work with MongoDB: https://mongoosejs.com/
- sequelize if you work with a relationa database: https://sequelize.org/
- express-async-handler to catch for handling exceptions inside of async Express routes: https://github.com/Abazhenov/Express-async-handler#readme
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:
- SuperTest provides a high-level abstraction for testing HTTP requests - perfect for APIs: https://github.com/visionmedia/supertest
- Mocha runs on both Node.js and the browser, making it useful for testing asynchronous functionality: https://mochajs.org/
- Chai is an assertion library that you can pair with other testing frameworks like Mocha: https://www.chaijs.com/
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:
- always look at https://developer.mozilla.org/en-US/docs/Web/JavaScript to know latest features and syntax
- take also a look at https://github.com/goldbergyoni/nodebestpractices that is a very detailed collection of best practices.