DEV Community

Chiranjib
Chiranjib

Posted on • Updated on

Create modular routes with express

Previous: Setting up a Node.js backend

DISCLAIMER: This guide by no means aspires to be the holy grail. At the time of writing this I've been developing software for more than a decade and let's say I know of a few things that would make coding easy when it comes to basic principles like Readability, Maintainability, etc.
P.S. If you don't like my suggestions, please don't hurl abuses at me, I have my friends for doing that. If you have some constructive feedback, please do share!

Assuming you have set up the Node.js backend boilerplate with express in accordance with the previous guide, let's go on and introduce some flavour.

Step 1 - enable body-parser

A crucial step before we dive into defining our routes, is to enable request body parsing in express. Mostly, the POST and PUT routes are expected to have a request body payload, and we need to be prepared to parse them.

Express.js includes the body-parser package now, but we need to plug this in to the base express router. Stating the obvious, this statement needs to be plugged in before any of the routes start processing, with the assumption that all routes would require this functionality.
One small detail to note here - we have introduced a new variable: REQUEST_BODY_SIZE_LIMIT. This helps control the maximum allowed request body limit. As application grows complex, this helps put restraints in place according to available infrastructure resources.

...
const { SERVER: { PORT, REQUEST_BODY_SIZE_LIMIT } } = require('_config');
...
app.use(express.json({ limit: REQUEST_BODY_SIZE_LIMIT }));
app.use(express.urlencoded({ extended: true, limit: REQUEST_BODY_SIZE_LIMIT }));
Enter fullscreen mode Exit fullscreen mode

Step 2 (optional) - implement some basic security measures

Application security is non-negotiable. Thanks to the community, there's a package for this as well. The name is aptly selected - helmet. Put this on before you go for the ride 😉 😉

npm i --save helmet

And we plug this in our code towards the beginning.

...
const helmet = require('helmet');
...
app.use(helmet());
app.use(bodyParser.json({ limit: REQUEST_BODY_SIZE_LIMIT }));
...
Enter fullscreen mode Exit fullscreen mode

Step 3 - decide on the approach

A very important but somewhat undervalued aspect of software development is to write code with a predefined structure. When the team size is small (<5), it seems manageable because communication is easier. But, as the team grows in size, people come in with different ideologies and beliefs. This has the potential of causing mayhem and a feeling like loss of identity.
An instrument to facilitate the same is Design Patterns. We will try to understand this step by step as we introduce our components.

This is what we will follow for the purposes of this example:

  • Entity: An object that supports CRUD operations
  • Controller: The mechanism of exposing the CRUD operations of an entity via endpoints of the application

Step 4 - define entities

We will implement the Decorator Design Pattern for our entities. The idea here is - our base entity will expose a few methods:

  • create()
  • read()
  • update()
  • delete()
  • getExtensions()

Each entity will have it's name, which will be used to form the controller that exposes the routes associated with the entity. You may read debates on the internet about using classes versus functions in JavaScript, I would say use either that you like. We are defining entities with classes, just because I am comfortable this way.

Base Entity

Create this file entities/BaseEntity.js

const Router = require('express').Router;

/**
 * @class
 */
class BaseEntity {
    constructor() {
        this.name = 'BaseEntity';
        this.extensions = [];
    }

    /**
     * The child class is supposed to override this
     * @returns {Object}
     */
    create() {
        throw 'Not Supported';
    }

    /**
     * The child class is supposed to override this
     * @returns {Object | Array.<Object>}
     */
    read() {
        throw 'Not Supported';
    }

    /**
     * The child class is supposed to override this
     * @returns {Object}
     */
    update() {
        throw 'Not Supported';
    }

    /**
     * The child class is supposed to override this
     * @returns {Object}
     */
    delete() {
        throw 'Not Supported';
    }

    /**
     * The child class is supposed to override this
     * @returns {Array.<Router>}
     */
    getExtensions() {
        return this.extensions;
    }
}

module.exports = BaseEntity;

Enter fullscreen mode Exit fullscreen mode
Entity One

We will set up our first Entity as a child of the BaseEntity. Create the file entities\entity1\index.js as:

const BaseEntity = require('_entities/BaseEntity');

module.exports = class EntityOne extends BaseEntity {
    constructor() {
        super();
        this.name = 'EntityOne';
        this.records = [
            { 'id': 1, 'key1': 'value1' },
            { 'id': 2, 'key2': 'value2' }
        ];
    }

    create(payload) {
        const lastObject = this.records[this.records.length - 1];
        this.records.push({ id: (lastObject?.id ?? 0) + 1, ...payload });
    }

    read(id) {
        if (id) {
            return this.records.find(record => record.id === parseInt(id));
        } else {
            return this.records;
        }
    }

    update(id, payload) {
        const record = this.records.find(record => record.id === parseInt(id));
        if (record) {
            const { _id, ...otherAttributes } = record;
            Object.assign(record, { ...otherAttributes, ...payload });
        }
    }

    delete(id) {
        const recordIndex = this.records.findIndex(record => record.id === parseInt(id));
        if (recordIndex) {
            this.records.splice(recordIndex, 1);
        }
    }
};

Enter fullscreen mode Exit fullscreen mode

Let's take a moment to see what we've done above. The BaseEntity defines CRUD methods with an error. The child entity creates overrides of the same methods, with the correct implementation. If the child does not create the implementation (intentionally or unintentionally), then the child would still respond with the implementation of the BaseEntity.

Step 5 - enable controller generation

We will leverage the Builder Design Pattern for enabling our controllers. Let's define a file controllers\index.js as:

const BaseEntity = require('_entities').getBaseEntity();
const logger = require('_utils/logger');

/**
 * 
 * @param {BaseEntity} entityObject 
 * @returns {[string, Router]}
 */
function getController(entityObject) {
    const router = require('express').Router({ mergeParams: true });
    router.post('/', async (req, res, next) => {
        try {
            const response = await entityObject.create(req.body);
            res.json(response);
        } catch(e) {
            logger.error(e);
            res.status(500).send('Oops! Something went wrong!');
        }
    });

    router.get('/', async (req, res, next) => {
        try {
            const response = await entityObject.read();
            res.json(response);
        } catch(e) {
            logger.error(e);
            res.status(500).send('Oops! Something went wrong!');
        }
    });

    router.get('/:entityObjectId', async (req, res, next) => {
        try {
            const response = await entityObject.read(req.params.entityObjectId);
            res.json(response);
        } catch(e) {
            logger.error(e);
            res.status(500).send('Oops! Something went wrong!');
        }
    });

    router.put('/:entityObjectId', async (req, res, next) => {
        try {
            const response = await entityObject.update(req.params.entityObjectId, req.body);
            res.json(response);
        } catch(e) {
            logger.error(e);
            res.status(500).send('Oops! Something went wrong!');
        }
    });

    router.delete('/:entityObjectId', async (req, res, next) => {
        try {
            const response = await entityObject.delete(req.params.entityObjectId);
            res.json(response);
        } catch(e) {
            logger.error(e);
            res.status(500).send('Oops! Something went wrong!');
        }
    });

    return [`/${entityObject.name}`, router];
}

module.exports = {
    getController
};

Enter fullscreen mode Exit fullscreen mode

This file defines a consistent way of generating endpoints. The idea is - to define controllers, we will be forced to think in terms of entities, which in turn facilitates greater control on CRUD operations.

Step 5 - generate controllers with entities

We are good to have our first entity based controller. Let's modify the index.js as follows:

...
const { getController } = require('_controllers');
...
app.get('/healthcheck', (req, res, next) => {
    res.send('OK');
});

app.use(...getController(new EntityOne()));

app.use('/', (req, res) => {
    res.send(`${req.originalUrl} can not be served`);
});
Enter fullscreen mode Exit fullscreen mode

Once we plug this in, EntityOne would expose four routes as:

  • POST /EntityOne
  • GET /EntityOne
  • PUT /EntityOne
  • DELETE /EntityOne

That's all for this one. In the next one, we will add more flavours to our setup.

Next: Integrate Node.js backend with MongoDB

Top comments (0)