How to include MongoDB client, common middleware and other 'bits and pieces' within separate-route, MVC-style Express.js application?
Asked Answered
S

0

0

I'm refactoring a big (~8,000 line) single file app into a 'separate-route', 'MVC-style' app.

I want to apply 'best practises' to the folder/file structure and the contents of app.js.

I'm trying to keep things simple, intuitive and minimal.

I've referred to several video tutorials, articles, posts and github repo's and grabbed what seems to be the most sensible parts of each (see links at end of post).

These learning resources have got me 90% of the way, but there are nuances in their examples and my implementation that don't quite match up.

They also don't seem to cover how to handle these requirements:

  1. MongoDB client configuration
  2. Socket.io implementation
  3. app.use() middleware used in all requests
  4. JWT token verification
  5. Simple template engine implementation

These are fairly common features, so I am hoping there are standard approaches to handling these requirements and also that this question will be valuable to others in the same boat.

Below is what I have so far.

Everything seems OK until I get to app.js.

structure

app.js <---- entry file  
routes
- index.js
- resource_01_route.js
- resource_02_route.js
- resource_03_route.js
// etc
middleware
- middleware_01.js
- middleware_02.js
- middleware_03.js
// etc  
views
- index.ntl  
config
- db_connection.js
- environment_variables.js
- local_settings.js    

routes/index.js

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

const resource_01_route = require('./resource_01_route');
const resource_02_route = require('./resource_02_route');
const resource_03_route = require('./resource_03_route');

router.use('/api/:api_version/resource_01', resource_01_route);
router.use('/api/:api_version/resource_02', resource_02_route);
router.use('/api/:api_version/resource_03', resource_03_route);

module.exports = router;

routes/resource_01_route.js

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

const middleware_01 = require('../middleware/middleware_01');
const middleware_02 = require('../middleware/middleware_02');
const middleware_03 = require('../middleware/middleware_03');

router
    .route('/')
    .get(middleware_01)
    .post(middleware_02)
    .delete(middleware_03);

// etc  

module.exports = router;

middleware/middleware_01.js

const { db_connection } = require('../config/db_connection');
db_connection.open();

const middleware_01 = async (req, res) => {
    try {
        const query = { username: 'my name' };
        const collection = db_connection.conn.db('users').collection('users');
        const result = await collection.findOne(query);
        res.json({ result: result });
    } catch (error) {
        console.log(error);
        res.status(500).json({ error });
    }
};

module.exports = middleware_01;

config/db_connection.js

const MongoClient = require('mongodb').MongoClient;  
const { connection_string } = require('./environment_variables');

class db_connection {
  static async open() {
    if (this.conn) return this.conn;
    this.conn = await MongoClient.connect(connection_string);
    return this.conn;
  }
}

db_connection.conn = null;
db_connection.url = connection_string;

module.exports = { db_connection };

The logic for the class above is from here.

config/environment_variables.js

let online_status, 
    PORT, 
    var_01, 
    var_02, 
    var_03, 
    var_04; // etc

if (process.env.MONGODB_USER) {
    online_status = 'online';
} else {
    online_status = 'offline';
    local_settings = require('./local_settings');
}

if (online_status === 'online') {
    PORT = 8080;
    var_01 = process.env.VAR_01;
    var_02 = process.env.VAR_02;
    var_03 = process.env.VAR_03;
    var_04 = process.env.VAR_04;
} else if (online_status === 'offline') {
    PORT = 3000;
    var_01 = local_settings.VAR_01;
    var_02 = local_settings.VAR_02;
    var_03 = local_settings.VAR_03;
    var_04 = local_settings.VAR_04;
}

module.exports = {
    online_status,
    PORT,
    var_01,
    var_02,
    var_03,
    var_04,
};

config/local_settings.js ignored by .gitignore

module.exports = {
    VAR_01: 'VAR_01',
    VAR_02: 'VAR_02',
    VAR_03: 'VAR_03',
    VAR_04: 'VAR_04',
};

My next step is to create a clean and minimal app.js, eg ideally something like:

const express = require('express');
const app = express();
const routes = require('./routes');

app.use('/', routes); // <---- call in the routes/index.js file   
  
app.listen(PORT, () => {
    console.log(`the app is listening`); 
});

This is where I am getting stuck.

I have, for the most part, got the route and middleware management out of app.js.

However, I still have a significant number of 'bits' left over in app.js.

I don't know how to include them (either within app.js or somewhere else to keep app.js clean).

This includes:

1) A reference to mongo_client that is used in multiple middleware functions
solution found, as shown above

2) socket.io logic, ie:

io.on('connection', async (socket) => {

/*

define LOTS of socket.io logic

needs access to mongo_client

*/

});

3) app.use() middleware that is used on all requests, ie:

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", 'https://static.codepen.io'],
        styleSrc: ["'self'", 'fonts.googleapis.com', "'unsafe-inline'"],
        fontSrc: ["'self'", 'fonts.gstatic.com'],
        frameSrc: ["'self'", 'https://codepen.io'],
      },
    },
  })
);

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static('dist'));

4) JWT verification logic, ie:

app.use((req, res, next) => {

/*

define all JWT verification logic  

needs access to mongo_client

*/

next();

});

5) Simple template engine definition and res.render, ie:

app.engine('ntl', (filePath, options, callback) => {
  fs.readFile(filePath, (err, content) => {
    if (err) return callback(err);
    var rendered = content
      .toString()
      .replace('#page_html#', options.page_html)
      .replace(/#site_title#/g, site_title)
      .replace(/#site_description#/g, site_description)
      .replace(/#og_image#/g, og_image)
      .replace(/#og_url#/g, og_url);
    return callback(null, rendered);
  });
});

app.set('views', './views');
app.set('view engine', 'ntl');

For full context, below is the remaining app.js file I am trying to refactor, in all its glory (please ignore combo ES6/common module handling):

import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';  

// needed for template engine
const fs = require('fs');

// needed for security
const helmet = require('helmet');

// needed for socket.io 
const app = express();
const http = require('http');
const server = http.createServer(app);
const io = require('socket.io')(server);
app.set('socketio', io);

// can't remember what this is for  
const path = require('path');

io.on('connection', async (socket) => {

/*

define LOTS of socket.io logic

needs access to mongo_client

 */

});

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", 'https://static.codepen.io'],
        styleSrc: ["'self'", 'fonts.googleapis.com', "'unsafe-inline'"],
        fontSrc: ["'self'", 'fonts.gstatic.com'],
        frameSrc: ["'self'", 'https://codepen.io'],
      },
    },
  })
);


app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static('dist'));

app.use((req, res, next) => {


/*

define all JWT verification logic  

needs access to mongo_client

 */

next();

});

// BEGIN simple templating engine
// see:  https://expressjs.com/en/advanced/developing-template-engines.html

app.engine('ntl', (filePath, options, callback) => {
  fs.readFile(filePath, (err, content) => {
    if (err) return callback(err);
    var rendered = content
      .toString()
      .replace('#page_html#', options.page_html)
      .replace(/#site_title#/g, site_title)
      .replace(/#site_description#/g, site_description)
      .replace(/#og_image#/g, og_image)
      .replace(/#og_url#/g, og_url);
    return callback(null, rendered);
  });
});

app.set('views', './views');
app.set('view engine', 'ntl');

// END simple templating engine

server.listen(PORT, () => {
  console.log(`running on port ${PORT}`);
});

Research

Video: How to Add a Route to an Express Server in Node.js

Video: Project Folder Structure in Express

Video: NodeJS REST: How to Refactor Code into Multiple Files with Router

Article: Better Express Routing & Architecture for NodeJS

Article: Express.js - Documentation - Routing

Repo: Express.js - GitHub Repo - Route Separation Example

Article: Express.js - Documentation - Writing middleware for use in Express apps

Article: Express.js - Documentation - Using middleware

Question: How to separate routes on Node.js and Express 4?

Question: How to include route handlers in multiple files in Express?

Question: Express js:multiple route files into single file

Sansbury answered 26/11, 2021 at 14:11 Comment(2)
In the project structure, I suggest separate modules for "database" (connect to database server) and "databaseOperations" (CRUD operations). In the app.js initialize the database connection, which could be used throughout the application (to perform the CRUD ops).Spue
In my opinion you can start with separating database connection first.Flory

© 2022 - 2024 — McMap. All rights reserved.