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:
- MongoDB client configuration
- Socket.io implementation
app.use()
middleware used in all requests- JWT token verification
- 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?
app.js
initialize the database connection, which could be used throughout the application (to perform the CRUD ops). – Spue