Understanding the MVC Pattern with Node.js

Abhishek Jaiswal - Nov 6 - - Dev Community

The Model-View-Controller (MVC) pattern is one of the most popular architectural patterns in web development. By dividing an application into three interconnected components — Model, View, and Controller — MVC promotes organized, maintainable, and scalable code. Node.js, with its asynchronous processing and vast ecosystem, is an excellent choice for building MVC-based applications, especially for web and API development. This guide explores the MVC pattern with Node.js, taking you through its benefits and a practical implementation.


What is the MVC Pattern?

The MVC pattern divides an application into three distinct parts:

  1. Model: Represents the data and business logic of the application. It interacts with the database and processes data independently of the user interface.
  2. View: Handles the presentation layer of the application. It displays data to the user and sends user commands to the Controller.
  3. Controller: Acts as an intermediary between the Model and the View. It takes user input, interacts with the Model, and updates the View accordingly.

This separation of concerns helps organize code in a way that’s easy to manage, test, and expand.


Benefits of Using MVC in Node.js

  1. Maintainability: MVC makes it easier to manage complex applications by organizing code into layers.
  2. Scalability: The structure allows individual components to be scaled or modified independently.
  3. Reusable Code: With MVC, components are often reusable across multiple views or parts of the application.
  4. Efficient Team Collaboration: Frontend and backend developers can work simultaneously with minimal overlap.

Setting Up an MVC Project with Node.js

Here, we’ll build a simple MVC application using Node.js and Express. Our example will be a basic "Task Manager" that allows users to view, add, and delete tasks.


Step 1: Initialize the Project

Start by creating a new Node.js project and installing necessary dependencies.

# Create a project folder
mkdir mvc-task-manager
cd mvc-task-manager

# Initialize Node.js
npm init -y

# Install Express and other dependencies
npm install express ejs mongoose body-parser
Enter fullscreen mode Exit fullscreen mode
  • Express: A minimalist web framework for Node.js, ideal for setting up controllers and routes.
  • EJS: A templating engine that allows you to render dynamic HTML views.
  • Mongoose: A popular library for MongoDB to model our data in the database.
  • Body-parser: Middleware to parse incoming request bodies in a middleware.

Step 2: Set Up the Project Structure

Organize the application into models, views, and controllers folders. This structure is essential for an MVC pattern.

mvc-task-manager/
│
├── models/
│   └── Task.js
│
├── views/
│   ├── index.ejs
│   └── tasks.ejs
│
├── controllers/
│   └── taskController.js
│
├── public/
│   └── css/
│       └── styles.css
│
├── app.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure the Model

The Model represents the data structure in your application. For this task manager, we’ll define a Task model in MongoDB using Mongoose.

// models/Task.js

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
    title: { type: String, required: true },
    description: { type: String },
    completed: { type: Boolean, default: false }
});

module.exports = mongoose.model('Task', taskSchema);
Enter fullscreen mode Exit fullscreen mode

Here, taskSchema defines our Task model with fields for title, description, and completed.

Step 4: Create the Controller

The Controller handles the application logic, processes user inputs, interacts with the Model, and directs the View to render the data.

// controllers/taskController.js

const Task = require('../models/Task');

exports.getTasks = async (req, res) => {
    const tasks = await Task.find();
    res.render('tasks', { tasks });
};

exports.createTask = async (req, res) => {
    const { title, description } = req.body;
    await Task.create({ title, description });
    res.redirect('/tasks');
};

exports.deleteTask = async (req, res) => {
    await Task.findByIdAndDelete(req.params.id);
    res.redirect('/tasks');
};
Enter fullscreen mode Exit fullscreen mode

This controller defines three main actions:

  • getTasks: Fetches all tasks from the database.
  • createTask: Adds a new task to the database.
  • deleteTask: Deletes a task by its ID.

Step 5: Set Up the View

In this example, we use EJS to render HTML views. This will allow us to display task data dynamically in the browser.

Create an index.ejs file for the homepage and a tasks.ejs file to display tasks.

<!-- views/index.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
    <h1>Welcome to Task Manager</h1>
    <a href="/tasks">View Tasks</a>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The tasks.ejs file will render a list of tasks and provide forms for adding or deleting tasks.

<!-- views/tasks.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task List</title>
</head>
<body>
    <h1>Tasks</h1>

    <ul>
        <% tasks.forEach(task => { %>
            <li><%= task.title %> - <a href="/delete/<%= task._id %>">Delete</a></li>
        <% }) %>
    </ul>

    <form action="/add" method="POST">
        <input type="text" name="title" placeholder="Task Title" required>
        <input type="text" name="description" placeholder="Description">
        <button type="submit">Add Task</button>
    </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 6: Set Up Routes

Define routes in the main app.js file to connect each URL endpoint to the relevant controller function.

// app.js

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const taskController = require('./controllers/taskController');

const app = express();
app.set('view engine', 'ejs');

mongoose.connect('mongodb://localhost:27017/taskDB', { useNewUrlParser: true, useUnifiedTopology: true });

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

// Routes
app.get('/', (req, res) => res.render('index'));
app.get('/tasks', taskController.getTasks);
app.post('/add', taskController.createTask);
app.get('/delete/:id', taskController.deleteTask);

const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Step 7: Styling with CSS

To add a bit of styling, create a styles.css file in the public/css/ folder. You can add basic styling to make your application visually appealing.


Step 8: Run and Test the Application

Start the application using:

node app.js
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000 in your browser. You’ll be able to navigate between views, add new tasks, and delete tasks using the MVC architecture.


Best Practices for MVC Architecture in Node.js

  1. Keep Controllers Thin: Business logic should primarily reside in the Model.
  2. Use Middleware for Reusable Code: For instance, use middleware for authentication or request logging.
  3. Separate Routes from Controllers: To keep controllers clean and focused, consider defining routes in a separate file.
  4. Modularize Your Code: Keep models, views, and controllers in separate files and folders to maintain structure.

. . . . . . . . . . . .