Skip to main content

Express MVC Pattern

Introduction

The Model-View-Controller (MVC) pattern is a software architectural pattern that separates an application into three main logical components: the Model, the View, and the Controller. Each of these components handles specific aspects of an application's development and maintenance.

When building Express.js applications, implementing the MVC pattern helps organize your code better, making it more maintainable, testable, and scalable. This approach is especially helpful as your application grows in size and complexity.

In this tutorial, we'll explore how to implement the MVC pattern in an Express.js application, complete with practical examples and best practices.

Understanding MVC Components

Before diving into implementation, let's understand what each component does:

  1. Model: Manages data, logic, and rules of the application. It communicates with the database and handles data validation.

  2. View: Handles the UI parts of the application. In Express, this typically involves rendering templates (like EJS, Pug, or Handlebars) or serving JSON data for front-end frameworks.

  3. Controller: Acts as an intermediary between Model and View. It processes incoming requests, interacts with the Model to get data, and passes that data to the View.

Setting Up an Express MVC Project

Let's create a simple Express application using the MVC pattern. We'll build a basic book management system.

Project Structure

First, let's establish our folder structure:

book-management-app/
├── controllers/
│ └── bookController.js
├── models/
│ └── Book.js
├── views/
│ ├── books/
│ │ ├── index.ejs
│ │ ├── show.ejs
│ │ ├── new.ejs
│ │ └── edit.ejs
├── routes/
│ └── bookRoutes.js
├── app.js
└── package.json

Setting Up the Project

Initialize the project:

bash
mkdir book-management-app
cd book-management-app
npm init -y
npm install express ejs mongoose body-parser

Creating the Main Application File

Let's create our app.js:

javascript
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const path = require('path');

// Initialize express app
const app = express();

// Connect to MongoDB
mongoose.connect('mongodb://localhost/bookmanagement', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('Could not connect to MongoDB', err));

// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Routes
const bookRoutes = require('./routes/bookRoutes');
app.use('/books', bookRoutes);

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Implementing the Model

Let's create our Book model in models/Book.js:

javascript
const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
author: {
type: String,
required: true,
trim: true
},
publishYear: {
type: Number,
required: true
},
description: {
type: String,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});

const Book = mongoose.model('Book', bookSchema);

module.exports = Book;

The model defines the structure of our data and interacts with the database. In this case, we're using Mongoose to create a Book schema that includes fields like title, author, and publish year.

Creating Controllers

Next, let's create our controller in controllers/bookController.js:

javascript
const Book = require('../models/Book');

// Display list of all books
exports.book_list = async (req, res) => {
try {
const books = await Book.find({}).sort({ createdAt: -1 });
res.render('books/index', {
title: 'All Books',
books: books
});
} catch (err) {
res.status(500).send('Error retrieving books: ' + err.message);
}
};

// Display detail page for a specific book
exports.book_detail = async (req, res) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
return res.status(404).send('Book not found');
}
res.render('books/show', {
title: book.title,
book: book
});
} catch (err) {
res.status(500).send('Error retrieving book: ' + err.message);
}
};

// Display book create form
exports.book_create_get = (req, res) => {
res.render('books/new', { title: 'Create New Book' });
};

// Handle book create on POST
exports.book_create_post = async (req, res) => {
try {
const book = new Book({
title: req.body.title,
author: req.body.author,
publishYear: req.body.publishYear,
description: req.body.description
});

await book.save();
res.redirect('/books');
} catch (err) {
res.status(400).render('books/new', {
title: 'Create New Book',
error: err.message
});
}
};

// Display book delete form
exports.book_delete_get = async (req, res) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
return res.status(404).send('Book not found');
}
res.render('books/delete', {
title: 'Delete Book',
book: book
});
} catch (err) {
res.status(500).send('Error retrieving book: ' + err.message);
}
};

// Handle book delete on POST
exports.book_delete_post = async (req, res) => {
try {
await Book.findByIdAndRemove(req.params.id);
res.redirect('/books');
} catch (err) {
res.status(500).send('Error deleting book: ' + err.message);
}
};

// Display book update form
exports.book_update_get = async (req, res) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
return res.status(404).send('Book not found');
}
res.render('books/edit', {
title: 'Update Book',
book: book
});
} catch (err) {
res.status(500).send('Error retrieving book: ' + err.message);
}
};

// Handle book update on POST
exports.book_update_post = async (req, res) => {
try {
await Book.findByIdAndUpdate(req.params.id, {
title: req.body.title,
author: req.body.author,
publishYear: req.body.publishYear,
description: req.body.description
});
res.redirect(`/books/${req.params.id}`);
} catch (err) {
res.status(400).render('books/edit', {
title: 'Update Book',
book: req.body,
error: err.message
});
}
};

The controller contains methods that handle specific actions like listing all books, showing details of a specific book, creating new books, etc. Each method interacts with the Model to fetch or update data and then passes that data to the appropriate View.

Setting Up Routes

Now let's create our routes in routes/bookRoutes.js:

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

// Require the controller
const book_controller = require('../controllers/bookController');

// Book routes
router.get('/', book_controller.book_list);
router.get('/new', book_controller.book_create_get);
router.post('/new', book_controller.book_create_post);
router.get('/:id', book_controller.book_detail);
router.get('/:id/edit', book_controller.book_update_get);
router.post('/:id/edit', book_controller.book_update_post);
router.get('/:id/delete', book_controller.book_delete_get);
router.post('/:id/delete', book_controller.book_delete_post);

module.exports = router;

The routes map URLs to controller methods, defining the API of our application.

Creating Views

Finally, let's create a sample view in views/books/index.ejs:

html
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
.book-list { list-style-type: none; padding: 0; }
.book-item { border: 1px solid #ddd; padding: 15px; margin-bottom: 10px; border-radius: 4px; }
.book-title { font-size: 18px; color: #0066cc; }
.book-author { font-size: 14px; color: #666; }
.book-year { font-size: 12px; color: #999; }
.add-btn {
display: inline-block;
background: #0066cc;
color: white;
padding: 10px 15px;
text-decoration: none;
border-radius: 4px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1><%= title %></h1>

<a href="/books/new" class="add-btn">Add New Book</a>

<% if (books && books.length > 0) { %>
<ul class="book-list">
<% books.forEach(function(book) { %>
<li class="book-item">
<div class="book-title">
<a href="/books/<%= book._id %>"><%= book.title %></a>
</div>
<div class="book-author">by <%= book.author %></div>
<div class="book-year">Published in <%= book.publishYear %></div>
</li>
<% }); %>
</ul>
<% } else { %>
<p>No books found.</p>
<% } %>
</body>
</html>

And let's create a view to display a specific book's details in views/books/show.ejs:

html
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
.book-detail { border: 1px solid #ddd; padding: 20px; border-radius: 4px; }
.book-title { font-size: 24px; color: #0066cc; margin-bottom: 10px; }
.book-author { font-size: 18px; color: #666; margin-bottom: 10px; }
.book-year { font-size: 16px; color: #999; margin-bottom: 20px; }
.book-description { line-height: 1.6; }
.nav-links { margin-top: 20px; }
.nav-links a {
display: inline-block;
padding: 8px 12px;
margin-right: 10px;
text-decoration: none;
color: white;
border-radius: 4px;
}
.back-btn { background: #666; }
.edit-btn { background: #0066cc; }
.delete-btn { background: #cc0000; }
</style>
</head>
<body>
<div class="book-detail">
<div class="book-title"><%= book.title %></div>
<div class="book-author">by <%= book.author %></div>
<div class="book-year">Published in <%= book.publishYear %></div>

<% if (book.description) { %>
<div class="book-description">
<%= book.description %>
</div>
<% } %>
</div>

<div class="nav-links">
<a href="/books" class="back-btn">Back to Books</a>
<a href="/books/<%= book._id %>/edit" class="edit-btn">Edit</a>
<a href="/books/<%= book._id %>/delete" class="delete-btn">Delete</a>
</div>
</body>
</html>

Views are responsible for presenting data to the user. In Express, views are typically template files that get rendered with data passed from controllers.

How MVC Works Together in Express

Let's trace a typical request flow through our MVC architecture:

  1. A user navigates to /books in their browser
  2. The request is routed to the book_list method in the bookController by our routes file
  3. The controller uses the Book model to fetch all books from the database
  4. The controller passes the books data to the index view
  5. The view renders the HTML with the book data
  6. The HTML response is sent back to the user's browser

This separation of concerns makes our code more maintainable and testable.

Best Practices for Express MVC

  1. Keep controllers focused: Controllers should handle HTTP requests and responses, delegating business logic to models or separate service modules.

  2. Fat models, skinny controllers: Put as much business logic as possible in your models, keeping controllers lightweight.

  3. Use middleware: Express middleware is great for cross-cutting concerns like authentication, logging, and error handling.

  4. Error handling: Implement consistent error handling across your application.

  5. Input validation: Validate all user inputs, preferably in the model or using middleware.

Real-World Example: RESTful API with MVC

Let's modify our book controller to support both HTML views and JSON responses for a RESTful API:

javascript
exports.book_list = async (req, res) => {
try {
const books = await Book.find({}).sort({ createdAt: -1 });

// Check the requested format
const format = req.query.format || 'html';

if (format === 'json') {
return res.json(books);
}

res.render('books/index', {
title: 'All Books',
books: books
});
} catch (err) {
if (req.query.format === 'json') {
return res.status(500).json({ error: err.message });
}
res.status(500).send('Error retrieving books: ' + err.message);
}
};

With this approach, clients can request data in different formats:

  • Browser users visiting /books will see the HTML page
  • API clients requesting /books?format=json will receive JSON data

Summary

In this tutorial, we've explored how to implement the MVC pattern in Express.js applications:

  • Models handle data access and business logic
  • Views present data to users
  • Controllers coordinate between models and views
  • Routes direct requests to the appropriate controller methods

The MVC pattern helps you organize your code for better maintainability, testability, and scalability. As your application grows, this organization will help keep your codebase manageable.

Further Resources and Exercises

Resources

Exercises

  1. Complete the application: Create the remaining views (new.ejs, edit.ejs, and delete.ejs) to complete the book management application.

  2. Add validation: Enhance the Book model with more sophisticated validation (e.g., ensure publish year is not in the future).

  3. Add user authentication: Implement a User model and authentication to restrict book creation/editing to logged-in users.

  4. API versioning: Modify your routes to support API versioning (e.g., /api/v1/books).

  5. Implement pagination: Update the book list controller to support paginated results.

By mastering the MVC pattern in Express, you'll build more maintainable applications and be better prepared for working on larger teams and projects.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)