A Minimalist Architecture Pattern for Express.js API Applications
Tags: productivity, javascript, express, webdev
Published: 2019-10-01 16:40:20

Express.js is a minimal web application framework that improves the productivity of web developers. It is very flexible and does not enforce any architecture pattern. This article demonstrates a new architecture pattern which I designed that will further improve your productivity.

## How to read this article
This article introduces a pattern that is different from the popular MVC or MSC (Model-Service-Controller) pattern. You can read this article before knowing any of those.

## Demo project  [GitHub](https://github.com/dividedbynil/ko-architecture)
Let's create a restaurant app RESTful API.

### Access rules
- Public users:
   * create an account
   * log in
- Members:
   * read all nearby available restaurants
- Owners:
   * CRUD all nearby restaurants
- Admins:
   * CRUD all nearby restaurants
   * CRUD all users

### Requirements
- Each restaurant object must have a name, geolocation coordinates, available status, and owner id.
- Each user object must have a name, email, user type (member/owner/admin), and password.

### Tech stack in this demo
- Database: MongoDB
- ORM: Mongoose

## JSON Response convention
When we send a JSON data back to the client, we may have conventions that identify a successful or failed operation, for example
```js
{
  success: false,
  error: ...
}
```
```js
{
  success: true,
  data: ...
}
```

Let's create functions for the JSON responses above.
### ./common/response.js
```js
function errorRes (res, err, errMsg="failed operation", statusCode=500) {
  console.error("ERROR:", err)
  return res.status(statusCode).json({ success: false, error: errMsg })
}

function successRes (res, data, statusCode=200) {
  return res.status(statusCode).json({ success: true, data })
}
```
Here we use default arguments for both functions, the benefit here is we can use the function as:
```js
errorRes(res, err)
successRes(res, data)
```
and we don't have to check if the optional arguments are null.
```js
// Example when default arguments not in use.
function errorRes (res, err, errMsg, statusCode) {
  if (errMsg) {
    if (statusCode) {
      ...
    }
    ...
  }
}

// or using ternary operator
function successRes (res, data, statusCode) {
  const resStatusCode = statusCode ? statusCode : 200
  ...
}
```
Feel free to replace `console.error` with logging function (from other library) you prefer.

## Database async callback convention
For create, read, update and delete operations, most database ORMs/drivers have a callback convention as:
```js
(err, data) => ...
```

knowing this, let's add another function in `./common/response.js`
### ./common/response.js
```js
function errData (res, errMsg="failed operation") {
  return (err, data) => {
    if (err) return errorRes(res, err, errMsg)
    return successRes(res, data)
  }
}
```

Export all functions in `./common/response.js`
```js
module.exports = { errorRes, successRes, errData }
```

## Database operations (CRUD) conventions
Let's define the database operations functions for all models. The conventions here are using `req.body` as the data source and `req.params._id` as collections' object id. Most of the functions will take a model and a list of populating fields as arguments, except delete operation (it is unnecessary to populate a deleted record). Since `delete` is a reserved keyword in JavaScript (for removing a property from an object), we use `remove` as the delete operation function name to avoid confliction.

### ./common/crud.js
```js
const { errData, errorRes, successRes } = require('../common/response')
const mongoose = require('mongoose')


function create (model, populate=[]) {
  return (req, res) => {
    const newData = new model({
      _id: new mongoose.Types.ObjectId(),
      ...req.body
    })
    return newData.save()
      .then(t => t.populate(...populate, errData(res)))
      .catch(err => errorRes(res, err))
  }
}

function read (model, populate=[]) {
  return (req, res) => (
    model.find(...req.body, errData(res)).populate(...populate)
  )
}

function update (model, populate=[]) {
  return (req, res) => {
    req.body.updated_at = new Date()
    return model.findByIdAndUpdate(
            req.params._id,
            req.body,
            { new: true },
            errData(res)
          ).populate(...populate)
  }
}

function remove (model) {
  return (req, res) => (
    model.deleteOne({ _id: req.params._id }, errData(res))
  )
}

module.exports = { read, create, update, remove }
```
The database CRUD function above used the functions from `./common/response`.

## Ready for development
With all the functions above defined, we are ready for application development. We now only require to define data models and routers.
Let's define the data models in `./models`

### ./models/Restaurant.js
```js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId
const validator = require('validator')


const restaurantSchema = new Schema({
  _id: ObjectId,
  name: { type: String, required: true },
  location: {
    type: {
      type: String,
      enum: [ 'Point' ],
      required: true
    },
    coordinates: {
      type: [ Number ],
      required: true
    }
  },
  owner: { type: ObjectId, ref: 'User', required: true },
  available: {
    type: Boolean,
    required: true,
  },

  updated_at: Date,
});

module.exports = mongoose.model('Restaurant', restaurantSchema, 'restaurants');
```

### ./models/User.js
```js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ObjectId = Schema.ObjectId
const validator = require('validator')


const userSchema = new Schema({
  _id: ObjectId,
  name: { type: String, required: true },
  email: {
    type: String,
    required: true,
    unique: true,
    validate: [ validator.isEmail, 'invalid email' ]
  },
  type: {
    type: String,
    enum: ['member', 'owner', 'admin'],
    required: true
  },
  password: { type: String, required: true, select: false },

  updated_at: Date,
});

module.exports = mongoose.model('User', userSchema, 'users');

```
The models above are very common, nothing new or fancy within.

## Routing and handlers
From the database convention above, you may think that using req.body as the data source is very limited if one needs a backend-process JSON field. Here we can use middleware to resolve the limitation.

### ./api/user.js
```js
router
.use(onlyAdmin)
.post('/', create(User))
.get('/all/:page', usersAtPage, read(User))
.put('/:_id', handlePassword, update(User))
.delete('/:_id', remove(User))
```

### ./api/restaurant.js
```js
const express = require('express')
const router = express.Router()
const { create, read, update, remove } = require('../common/crud')
const Restaurant = require('../models/Restaurant')

router
.get('/all/:lng/:lat/:page', nearBy(), read(Restaurant, ['owner']))
.get('/available/:lng/:lat/:page',
  nearBy({ available: true }),
  read(Restaurant, ['owner'])
)

function nearBy (query={}) {
  return (req, res, next) => {
    const { lng, lat, page } = req.params
    req.body = geoQuery(lng, lat, query, page)
    next()
  }
}
```

### ./api/auth.js
```js
router
.post('/signup', isValidPassword, hashPassword, signUp)
.post('/login', isValidPassword, findByEmail, verifyPassword, login)

// middlewares below are used for processing `password` field in `req.body`
function isValidPassword (req, res, next) {
  const { password } = req.body
  if (!password || password.length < 6) {
    const err = `invalid password: ${password}`
    const errMsg = 'password is too short'
    return errorRes(res, err, errMsg)
  }
  return next()
}

function hashPassword (req, res, next) {
  const { password } = req.body
  bcrypt.hash(password, saltRounds, (err, hashed) => {
    if (err)
      return errorRes(res, err, 'unable to sign up, try again')
    req.body.password = hashed
    return next()
  })
}

function signUp (req, res) {
...
}

function findByEmail (req, res, next) {
....
}

function verifyPassword (req, res, next) {
  ...
}

function login (req, res) {
  ...
}

module.exports = router;

```

## How to extend
Extending the application only requires to add new models and define new routers for endpoints.

## Differences from MSC
The Model-Service-Controller pattern requires every database model to have a set of service functions for data operations. And those service functions are only specifically defined for a particular model. With the new architecture above, we skip the definition of service functions for each model by reusing the common database operations functions, hence improving our productivity.

## Summary
This architecture provides great flexibility for customization, for example, it does not enforce a folder structure other than having a `common` folder, you are free from putting all middleware functions in router files or separating them by your rules. By using and extending the functions in the `common` folder, you can either start a project from scratch or refactor/continue a large project productively. So far I have been using this architecture for any size of ExpressJS projects.
{% github dividedbynil/ko-architecture %}