How to Build an MVC Application with Express
Model-View-Controller (MVC) is a popular software architecture pattern that separates the application into three main components: the model, the view, and the controller. The model represents the data and business logic of the application, the view represents the user interface, and the controller handles user input and interactions. In this article, we’ll show you how to build an MVC application with express, a popular web framework for Node.js.
Step 1: Install express and Setup Project
To get started, you’ll need a blank folder that you want to build a project in. Open a new terminal window and navigate to the base of your new project and run:
npm init
That’ll set up NPM for your project by creating a package.json file in the root of your project.
Next, run the following commands to install Express and related dependencies:
npm install express helmet compression cors xss-clean dotenv joi validator bcryptjs nodemailer winston --save
In addition to allowing you to create an HTTP server, the dependencies will also allow for gzip compression, cross-origin protection, cross-site scripting sanitation, SMTP (email sending), field validation, and logging.
Create Static Documents Folder
At the root of your project, create a folder called “public”. Inside, you can create an “index.html” file if you wish, upload static documents/files, and organize your central distribution network. Place all your client-side javascript inside of the “public” folder (preferably inside of a “js” folder). All static documents inside the “public” folder will be mapped to the root url of your application.
Create Config
At the root of your project, create a folder called “config”, create a file inside called “config.js”, add the following:
const dotenv = require('dotenv');
const path = require('path');
const Joi = require('joi');
dotenv.config({ path: path.join(__dirname, '../../.env') });
// Enforce type on the following properties:
const envVarsSchema = Joi.object()
.keys({
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().description('database server hostname (usually localhost)'),
DB_USERNAME: Joi.string().description('username to connect to database'),
DB_PASSWORD: Joi.string().description('password to connect to database'),
DB_NAME: Joi.string().description('name of database'),
SMTP_HOST: Joi.string().description('server that will send the emails'),
SMTP_PORT: Joi.number().description('port to connect to the email server'),
SMTP_USERNAME: Joi.string().description('username for email server'),
SMTP_PASSWORD: Joi.string().description('password for email server'),
EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),
})
.unknown();
const { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
// Define Config
module.exports = {
env: envVars.NODE_ENV,
port: envVars.PORT,
db: {
host: envVars.DB_HOST,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_NAME,
/* Uncomment if using MongoDB
settings: {
useNewUrlParser: true,
useUnifiedTopology: true
} */
},
email: {
smtp: {
host: envVars.SMTP_HOST,
port: envVars.SMTP_PORT,
auth: {
user: envVars.SMTP_USERNAME,
pass: envVars.SMTP_PASSWORD,
},
},
from: envVars.EMAIL_FROM,
}
};
Next, create a file called “logger.js” inside of the “config” folder, this will set up the logger, add the following contents:
/*
Logger Config
@author: hagopj13
@Source: https://github.com/hagopj13/node-express-boilerplate/blob/master/src/confog/logger.js
*/
const winston = require('winston');
const config = require('./config');
const enumerateErrorFormat = winston.format((info) => {
if (info instanceof Error) {
Object.assign(info, { message: info.stack });
}
return info;
});
const logger = winston.createLogger({
level: config.env === 'development' ? 'debug' : 'info',
format: winston.format.combine(
enumerateErrorFormat(),
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
winston.format.splat(),
winston.format.printf(({ level, message }) => `${level}: ${message}`)
),
transports: [
new winston.transports.Console({
stderrLevels: ['error'],
}),
],
});
module.exports = logger;
Create Routes
At the root of your project, create a folder called “routes”, create a file inside called “index.js”, add the following contents:
const express = require('express');
//const myRoute = require('./my.route');
const welcomeRoute = require('./welcome.route');
const config = require('../config/config');
const router = express.Router();
// { path: '/my', route: myRoute }
const defaultRoutes = [
{
path: '/',
route: welcomeRoute
},
];
defaultRoutes.forEach((route) => {
router.use(route.path, route.route);
});
module.exports = router;
Next, create a file in the same directory (routes) called “welcome.route.js”, add the following contents:
const express = require('express');
const welcomeController = require('../../controllers/welcome.controller');
const router = express.Router();
router.route('/').get(welcomeController.example);
module.exports = router;
Step 2: Create the model
The model is the component of the MVC architecture that represents the data and business logic of the application. In an express application, the model is usually implemented as a database or data store, such as a SQL database or a NoSQL database like MongoDB.
To begin, create a folder inside the root of your project called “models”, inside create a file called “index.js” and add the following contents:
module.exports.User = require('./user.model');
To create a model for your application, you’ll need to install and configure the appropriate database library and create the necessary database tables or collections. For example, to use MongoDB as your database, you’ll need to install the MongoDB driver and create a MongoDB collection for your model data.
MySQL/MariaDB
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.
To use Mongoose with a MySQL database, you need to use an additional library that allows MySQL to be used as a data source for Mongoose. One such library is mongoose-mysql-connector
.
Here’s an example of how you can use Mongoose with a MySQL database in a Node.js and Express application:
- Install the required packages:
npm install mongoose mongoose-mysql-connector
- Add the following code to index.js to connect to MySQL:
const mongoose = require('mongoose');
const mysqlConnector = require('mongoose-mysql-connector');
const config = require('../config/config');
let server;
mongoose.connect(mysqlConnector, {
host: config.db.host,
user: config.db.username,
password: config.db.password,
database: config.db.database
}).then(() => {
logger.info('Connected to database');
server = app.listen(config.port, () => {
logger.info(`Listening to port ${config.port}`);
});
});
const exitHandler = () => {
if (server) {
server.close(() => {
logger.info('Server closed');
process.exit(1);
});
} else {
process.exit(1);
}
};
const unexpectedErrorHandler = (error) => {
logger.error(error);
exitHandler();
};
process.on('uncaughtException', unexpectedErrorHandler);
process.on('unhandledRejection', unexpectedErrorHandler);
process.on('SIGTERM', () => {
logger.info('SIGTERM received');
if (server) {
server.close();
}
});
- Create a model for every type of data you wish to model, create a file inside “models” called “user.model.js”, add the following contents:
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Invalid email');
}
},
},
password: {
type: String,
required: true,
trim: true,
minlength: 8,
validate(value) {
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
throw new Error('Password must contain at least one letter and one number');
}
}
},
isEmailVerified: {
type: Boolean,
default: false,
},
date: {
type: Date,
default: Date.now
}
});
/**
* Check if email is taken
* @param {string} email - The user's email
* @param {ObjectId} [excludeUserId] - The id of the user to be excluded
* @returns {Promise<boolean>}
*/
userSchema.statics.isEmailTaken = async function (email, excludeUserId) {
const user = await this.findOne({ email, _id: { $ne: excludeUserId } });
return !!user;
};
/**
* Check if password matches the user's password
* @param {string} password
* @returns {Promise<boolean>}
*/
userSchema.methods.isPasswordMatch = async function (password) {
const user = this;
return bcrypt.compare(password, user.password);
};
/**
* @typedef User
*/
const User = mongoose.model('User', userSchema);
module.exports = User;
- Use the model to query and manipulate the data in the MySQL database:
// Find all users
User.find({}, (err, users) => {
if (err) throw err;
console.log(users);
});
// Create a new user
const newUser = new User({
name: 'John Doe',
email: 'john@doe.com',
password: 'password123'
});
newUser.save((err) => {
if (err) throw err;
console.log('User saved successfully');
});
// Update a user
User.findOneAndUpdate(
{ email: 'john@doe.com' },
{ name: 'John Smith' },
(err) => {
if (err) throw err;
console.log('User updated successfully');
}
);
// Delete a user
User.findOneAndDelete({ email: 'john@doe.com' }, (err) => {
if (err) throw err;
console.log('User deleted successfully');
});
MongoDB
Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. It helps you use the MongoDB database with Node.js by providing a straight-forward, schema-based solution to model your application data.
- To use Mongoose with Node.js and Express, you will first need to install Mongoose using the following command:
npm install mongoose
- Add the following code to app.js to connec to to MongoDB:
const mongoose = require('mongoose');
const config = require('./config/config');
let server;
mongoose.connect(config.database.host, config.database.settings).then(() => {
logger.info('Connected to database');
server = app.listen(config.port, () => {
logger.info(`Listening to port ${config.port}`);
});
});
const exitHandler = () => {
if (server) {
server.close(() => {
logger.info('Server closed');
process.exit(1);
});
} else {
process.exit(1);
}
};
const unexpectedErrorHandler = (error) => {
logger.error(error);
exitHandler();
};
process.on('uncaughtException', unexpectedErrorHandler);
process.on('unhandledRejection', unexpectedErrorHandler);
process.on('SIGTERM', () => {
logger.info('SIGTERM received');
if (server) {
server.close();
}
});
- Create a model for every type of data you wish to model, create a file inside “models” called “user.model.js”, add the following contents:
const userSchema = new mongoose.Schema({
name: String,
email: String,
password: String
});
const User = mongoose.model('User', userSchema);
module.exports = User;
- You can then use the
User
model to create a new user document like this:
const user = new User({
name: 'John',
email: 'john@example.com',
password: 'password'
});
user.save(function(error) {
if (error) {
console.log(error);
} else {
console.log('User saved successfully!');
}
});
- You can also use the model to query the collection for documents and update or delete them. For example, here’s how you can find a user by their email and update their name:
User.findOne({ email: 'john@example.com' }, function(error, user) {
if (error) {
console.log(error);
} else {
user.name = 'Jane';
user.save(function(error) {
if (error) {
console.log(error);
} else {
console.log('User updated successfully!');
}
});
}
});
Step 3: Create the view
The view is the component of the MVC architecture that represents the user interface of the application. In an express application, the view is typically implemented using a template engine like Pug, EJS, or Handlebars.
To create a view for your application, you’ll need to install the appropriate template engine and create a template file for each of the views in your application. For example, if you’re using EJS as your template engine, you might create a template file called index.ejs
for the main view of your application.
Step 4: Create the controller
The controller is the component of the MVC architecture that handles user input and interactions. In an express application, the controller is typically implemented as a set of routes and middleware functions that handle HTTP requests and responses.
To create a controller for your application, you’ll need to create a set of routes and middleware functions that handle the different actions of your application. For example, you might create a route that handles a GET request to the root path of your application and renders the main view using the appropriate template file.
Start by creating a folder in the root of your project called “controllers”. Create a file inside called “index.js”, add the following contents:
// module.exports.myController = require('./my.controller');
module.exports.welcomeController = require('./welcome.controller');
Next, create a file inside “controllers” called “welcome.controller.js”, add the following contents:
//const { emailService } = require('../services');
const example = (req, res) => {
res.send('Hello World!');
};
Step 5: Set up the express app
To set up the express app, you’ll need to create an app.js
file and configure the necessary middleware and routes. Here's an example of a simple express app that sets up a basic MVC structure:
app.js
const path = require('path');
const express = require('express');
const helmet = require('helmet');
const xss = require('xss-clean');
const compression = require('compression');
const cors = require('cors');
const config = require('./config/config');
const routes = require('./routes');
const app = express();
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(xss());
app.use(compression());
app.use(cors());
app.options('*', cors());
app.use('/', routes);
app.use(express.static(path.join(__dirname, 'public')));
// send back a 404 error for any unknown api request
app.use((req, res, next) => {
next(new ApiError(httpStatus.NOT_FOUND, 'Not found'));
});
module.exports = app;
index.js
const app = require('./app');
const config = require('./config/config');
const logger = require('./config/logger');
app.listen(config.port, () => {
console.log(`Example app listening on port ${config.port}`)
})
This app sets up express with JSON and URL-encoded body parsing middleware, and defines two routes: a GET route that renders the main view, and a POST route that creates a new item in the model.
Create Services
At the root of your project, create a folder called “services”, we will be adding a simple email service to illustrate how to create services. Create a file inside called “index.js”, add the following contents:
module.exports.emailService = require('./email.service');
Next, create a file inside called “email.service.js”, add the following contents:
/*
Email Service
@author: hagopj13
@Source: https://github.com/hagopj13/node-express-boilerplate/blob/master/src/services/email.service.js
*/
const nodemailer = require('nodemailer');
const config = require('../config/config');
const logger = require('../config/logger');
const transport = nodemailer.createTransport(config.email.smtp);
/* istanbul ignore next */
if (config.env !== 'test') {
transport
.verify()
.then(() => logger.info('Connected to email server'))
.catch(() => logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'));
}
/**
* Send an email
* @param {string} to
* @param {string} subject
* @param {string} text
* @returns {Promise}
*/
const sendEmail = async (to, subject, text) => {
const msg = { from: config.email.from, to, subject, text };
await transport.sendMail(msg);
};
/**
* Send reset password email
* @param {string} to
* @param {string} token
* @returns {Promise}
*/
const sendResetPasswordEmail = async (to, token) => {
const subject = 'Reset password';
// replace this url with the link to the reset password page of your front-end app
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
const text = `Dear user,
To reset your password, click on this link: ${resetPasswordUrl}
If you did not request any password resets, then ignore this email.`;
await sendEmail(to, subject, text);
};
/**
* Send verification email
* @param {string} to
* @param {string} token
* @returns {Promise}
*/
const sendVerificationEmail = async (to, token) => {
const subject = 'Email Verification';
// replace this url with the link to the email verification page of your front-end app
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
const text = `Dear user,
To verify your email, click on this link: ${verificationEmailUrl}
If you did not create an account, then ignore this email.`;
await sendEmail(to, subject, text);
};
module.exports = {
transport,
sendEmail,
sendResetPasswordEmail,
sendVerificationEmail,
};
Step 6: Test the MVC app
To test the MVC app, start the server by running the following command in your terminal:
node app.js
Then, visit the root URL of your app in your browser (e.g. http://localhost:3000
). You should see the main view of your app, and you should be able to create new items by submitting a form or making a POST request to the /create
route.
Download the Starter Project
Click Here to download/clone the starter project off of GitHub.