Simple and Minimalist MVC Architecture Pattern for Node & Express
--
Update —
This pattern still works and is effective, that being said I have an article that is in Alpha with a newer more complete pattern posted below
Now back to our regularly scheduled programming.
For some reason most examples of Node/Express MVC architecture are extremely over complicated, rely on third party packages, or are way to long and boring to follow.
Even the example from the Express repo, while cool, does not demonstrate MVC at its simplest.
TL;DR
All though this article is short and to the point, if you’re like me, you only really want an example that can be easily duplicated. If that’s you, here you go.
Routes
// routes/app.routes.js"use strict";const ctrl = require('../controllers/my-controller.controller.js');module.exports = function (app) {
app.get('api/v1/getdata', ctrl.getData); //Automatically passes req/res.
};
Controller
// controllers/my-controller.controller.js"use strict";const Promise = require('bluebird');
const fs = require('fs');let controller = {
getData: function (req, res) {
let rs = fs.createReadStream('data.json');
rs.on('error', (err) => {
res.status(500).send(err);
});
rs.on('open', () => {
rs.pipe(res);
});
}
};module.exports = controller;
Still here? The goal for the rest of this article is to create a small API that will download and upload objects from S3 using three components:
- A bare bones server to mount a route module.
- A module containing routes.
- A controller module that routes get mapped to.
First things first, run npm init
to initialize your project.
Install some dependencies npm i express morgan method-override body-parser cors aws-sdk --save
Next, create three files
controllers/s3.controller.js
routes/s3.routes.js
server.js
Let’s work backwards to build our MVC API and start with the controller file you just created. In controllers/s3.controller.js
create the following code.
"use strict";const AWS = require('aws-sdk');
const fs = require('fs');
let controller = {
getObjectS3: function (req, res) {let s3 = new AWS.S3();
let params = {
Bucket: 'myBucket',
Key: req.body.key
};
let doc = fs.createWriteStream(req.body.filepath);
s3.getObject(params, (err, data) => {
if (err) {
return res.status(401).send(err);
}
})
.createReadStream()
.pipe(res);},
uploadObjectS3: function (req, res) {let body = fs.createReadStream(req.body.filepath);
var s3obj = new AWS.S3({
params: {
Bucket: 'myBucket',
Key: req.body.key
}
});
s3obj.upload({
Body: body
})
.send(function (err, data) {
if (err) {
res.status(401).send(err);
}
res.status(200).send(data);
});}
};
module.exports = controller;
This is a simple controller file to define some functions that will be mapped to our routes. In this case, we do some basic data work to grab and upload files to S3.
Note: This code will not work unless you define AWS environment variables for access management.
We have already defined all the business logic with our controller, now comes the easy part. We simply have to tell browser based applications where to find the controller on the server. All we need are a few simple lines of code to define our routes in /routes/s3.routes.js
"use strict";
const ctrl = require('../controllers/s3.controller.js');
module.exports = function(app) {
app.get('api/v1/s3/getfile', ctrl.getObjectS3); //Automatically passes req/res.
app.post('api/v1/s3/uploadfile', ctrl.uploadObjectS3); //Automatically passes req/res.
};
Easy! The 2nd argument of Express routes simply take a function, and that’s all we’re really doing. We pull in the controller and assign it to a const
as ctrl
. We define the path as normal, then simply map the function with ctrl.getObjectS3
. Express passes in the req/res argument for us automatically and lets us use them.
Lastly, create a file called server.js
that will serve our basic API.
"use strict"; //For use with ES6
const express = require('express');
const app = express();
var server = require('http').createServer(app);
const morgan = require('morgan');
const bodyParser = require('body - parser');
const methodOverride = require('method - override');
const cors = require('cors');
const mongoose = require('mongoose');
app.use(morgan('dev')); // log with Morgan
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.text());
app.use(bodyParser.json({
type: 'application / vnd.api + json'
}));
app.use(methodOverride());
app.use(cors()); //enable CORS
//Mount our route file that we have yet to create. Note how we pass the instance of 'app' to the route.
const appRoutes = require('. / routes / s3.routes.js')(app)
server.listen(5000, () => {
console.log('Server listening at port 5000');
});
Nothing special going on here. Simply starting the server and mounting our route.
That’s really all there is to it. If you wanted to take this a step further, you could create a module to hold your S3 functions, which would most likely be reusable in the real world, and map your controller functions to them. In this example, that would be overkill, but comes in handy once you start getting into the bigger, more cumbersome apps.