In this post I’ll present a suggested design pattern and implementation for this design pattern using a Node + Express REST API with ES Classes. Personally, I hate writing the same code again and again. It violates the DRY principle and I hate to waste my time and my customers’ time. Being a C++ developer in background, I love a nice class design.

In today’s microservices and web, REST endpoints have become somewhat of the de-facto way to connect services and web applications. There are loads of examples how to create REST endpoints and servers using Node.js and Express 4.0. SOAP, which was popular a while back, has given way to JSON. New technologies like GraphQL have not made it to mainstream yet, so for now we are stuck with REST and JSON.

I haven’t found a tutorial that discusses how to do this using ES6 classes and a good class design. This is what we will cover today.

Rather than building REST endpoints over and over, my concept is to have a base router implement base behavior for the REST endpoint, then have derived classes override such behavior if needed.

We create an Abstract Base Class, with all the default route handlers as static methods. Those will take a request, process it (most likely read / write / delete / update the DB) and return the results. Then the SetupRoutes, will be the glue that binds the static methods to the actual routes. In addition our constructor will take a route name which will be the route path that will be processed.

Then derived classes can either disable certain routes, or override routes as need be, while maintaining the base behaviour, if that is what is needed (for example when wrapping a service, or doing simple DB operations).

RESTClassDesign

Now let’s implement this in JavaScript using Node.js, Express and ES Classes. I’m going to implement this example using MongoDB and Mongoose, but you can use any other DB or service you wish. The Mongoose in this code sample is pretty meaningless, it’s just for the sake of the example.

Create a new project folder, and call npm init inside it.
Then install express and required libs: npm install express body-parser cors bluebird mongoose change-case require-dir --save

Then I’ll create the server.js main file (we won’t discuss this in detail, as it’s mostly a node/express server. The one line that’s important to note is require('./routes/index')(server,db); as this will create all the routes for our application).


// Far from perfect, but a good base example for a server.
// Should also change console.log to some logger.
//server .js
'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const process = require('process');
const mongoose = require('mongoose');
const server = express();

server.use(bodyParser.urlencoded({ extended: true }));
server.use(bodyParser.json());
server.use((req, res, next) => {
 console.log(`${req.method} request on ${req.url}`);
next();
});

const db = mongoose.connect('mongodb://localhost');

//support for cross origin requests
server.use(cors({
 origins: '*',
 credentials: true,
 methods: ['GET', 'PUT', 'DELETE', 'POST', 'OPTIONS'],
 headers: ['X-Requested-With'],
}));


server.on('uncaughtException', (req, res, route, err) => {
 console.log(`Internal Server Error ${err}`);
res.send(500, { message: 'Internal Server Error' });
});

const PORT = 8080;
server.listen(PORT, '0.0.0.0', () => console.log(`REST server listening on PORT`));

// health check path
server.get('/status', (req, res, next) => {
 res.sendStatus(200, 'ok');
});

require('./routes/index')(server,db); //<===== includes all routes. server.get('*', (req, res, next) => {
 res.status(404).send('route not defined');
return next();
});

function cleanup() {
 //do server cleanup.
}

// listen for TERM signal .e.g. kill
process.on('SIGTERM', cleanup);

// listen for INT signal e.g. Ctrl-C
process.on('SIGINT', cleanup);

// export for testing
module.exports = server;

I’m including a single route file, which will build up all our routes. So let’s look into that index.js file, to see what’s going on in there:

// routes/index.js'
'use strict';


const routeHandlers = require('require-dir')('./route-handlers');
const changeCase = require('change-case');

const BASEURL = '/api/';

function setupRoutes(server,db) {


 // Initialize all routes by iterating the keys of the require-dir
 Object.keys(routeHandlers).forEach((routeName) => {
 //connect routes to the server base url
 const newRouteHandlerClass = new routeHandlers[routeName](db);
 server.use(`${BASEURL}${changeCase.paramCase(routeName)}`, newRouteHandlerClass.setupRoutes());
 });
};

module.exports = setupRoutes;

I like to use automatic glue code, rather thant re-type or build a static array. This way we have the system detect new routes and add them automatically, just by adding a file to a folder.

  1. I’m using require-dir which will include all route handlers. I wanted each route to handle it’s own paths, and not the global paths (I like encapsulation). So as a design decision I made the filename the subroute file.
  2. I then create an instance of the route handler class, passing it a reference to the dbDB (so it can do it’s thing).
  3. setupRoutes() returns a router, which I then connect to our server. I’m building on server.use of the express router , to bind routes to the baseurl. If you adpot this impementation you can always use your own structure.

Next let’s look at the base-router-handler which is the base to all route handlers. It will contain most of the code for any endpoint:

//routes/base-route-handler.js
'use strict';

const express = require('express');
const coWrapper = require('../utils/expressCoWrapper');


class BaseRouteHandler {
 constructor(collectionName,db) {
 this.db = db;
 this.router = new express.Router();
 this.collectionName = collectionName;
 this.collection = this.db[this.collectionName];
 this.setupMiddleware();
 }

 static validateOkResponse(res, foundItems) {
 if (!foundItems || !foundItems.length) {
 res.status(404).send('item not found');
 return false;
 }
 return true;
 }

 setupMiddleware() {
 // attach any middleware you might need on a route baseis,; can be overriden in subclasses
 //this.router.use(someMiddlewareFunction);
 }

 static* getSingle(req, res, next) {
 try {
 const foundItems = yield this.collection.find({id: req.params.id});
 if (BaseRouteHandler.validateOkResponse(res, foundItems)) res.json(foundItems[0]);
 } catch (err) {
 res.status(500).send('Internal Error');
 throw err;
 }
 }

 static* putSingle(req, res, next) {
 try {
 const result = yield this.collection.insert([req.body]);
 if (BaseRouteHandler.validateOkResponse(res, result)) res.json(result[0]);
 } catch (err) {
 res.status(500).send('Internal Error');
 throw err;
 }
 }

 static* deleteSingle(req, res, next) {

 const result = this.mongooseCollection.remove({id : req.params.id});
 res.json(result);

 }

 static* getMultiple(req, res, next) {
 try {
 res.connection.setTimeout(0); // disable server timeout - this may take a while
 const result = yield this.collection.find({});
 res.json(allItems);
 } catch (err) {
 res.status(500).send('Internal Error');
 throw err;
 }
 }

 static* postMultiple(req, res, next) {
 try {
 const result = yield this.collection.update([req.body]);
 res.json(result);
 } catch (err) {
 res.status(500).send('Internal Error');
 throw err;
 }
 }

 // eslint-disable-next-line require-yield
 static* notImplemented(req, res, next) {
 res.status(501).send('Not implemented');
 }

 setupRoutes() {
 const self = this;
 this.router.route('/:id')
 .get(coWrapper(self.constructor.getSingle))
 .put(coWrapper(self.constructor.putSingle))
 .delete(coWrapper(self.constructor.deleteSingle))
 .patch(coWrapper(self.constructor.notImplemented))
 .post(coWrapper(self.constructor.notImplemented));

 this.router.route('/')
 .get(coWrapper(self.constructor.getMultiple))
 .post(coWrapper(self.constructor.postMultiple()))
 .put(coWrapper(self.constructor.notImplemented))
 .patch(coWrapper(self.constructor.notImplemented))
 .delete(coWrapper(self.constructor.notImplemented));

 return this.router;
 }
}

module.exports = BaseRouteHandler;

I wanted to use generators, as I like their async / await like structure. So I wrote a co-wrapper file that will handle errors and the generators’ routes correctly, including wrapping with a promise. I do not wish to go into depths explaining it, as it’s not the point of this post. But you can see this file, in the git repo.

Next we create the base constructor, which takes the route name and (?). It creates the binding to a collection / table / service / anything else you want. It also calls the middleware setup; if you wish to bind your route based middleware, you can override this function in derived classes.

Next I go through and create static route handlers for each route. As you can see the route handlers are pretty simple: take json in, perform some DB operation and return the result. In other examples you might have more complex behaviour. The nice thing is the base creates a default behaviour, but by overriding the static methods in dervied classes we can do whatever we wish to do.

Once the baseclass is ready we can now create a real route, that will do something!
Let’s create a ‘route-handlers’ folder inside the ‘routes’ folder and add a file called companies.js.


//routes/route-handlers/companies.js
'use strict';

const BaseRouteHandler = require('../base-route-handler');


class CompaniesRouter extends BaseRouteHandler {
  constructor(db) {
    super('companies', db);
  }

  static* putSingle(req, res, next) {
    yield* super.notImplemented(req, res, next);
  }

  static* deleteSingle(req, res, next) {
    yield super.notImplemented(req, res, next);
  }

  static* postSingle(req, res, next) {
   // do some code to send an email to the admin, to ask to create multiple new companies
  }
}

module.exports = CompaniesRouter;

First look at how easy it was to create a new route. We didn’t need to write even this much code. We could just create the constructor and be done with it, if we wanted the same behaviour as the base class.

I did want to show, though, how easy it is to override the code without requiring much work. The base class provided us with a basic implementation for notImplemented[is “basic” an adjective instead of a specific type of implementation?], which makes it easy to disable routes.

Even adding a route is easy. Just add a handler implementation of your own. Makes it easy to test just the functionality and not have to re-write the same code over and over.

That’s all for now!

Hope your enjoyed this, or found this useful.