Production Engineering/
Lesson

If you've opened any backend codebase and seen folders named models/, views/, and controllers/, you've already met MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them.. It's a decades-old pattern that keeps showing up because it solves a real problem: where does each piece of code live? Understanding MVC gives you a mental map for reading and building server-side applications.

The three parts

MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them. stands for Model-View-Controller. Each part owns a specific slice of responsibility, and the rule is simple, don't let them bleed into each other.

Model

The Model is your data layer. It knows about your database, your business rules, and your validation logic. It does not know that an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request even happened.

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  validate() {
    if (!this.email.includes('@')) {
      throw new Error('Invalid email');
    }
    if (this.name.length < 2) {
      throw new Error('Name too short');
    }
  }

  async save() {
    this.validate();
    await database.users.insert(this);
  }

  static async findById(id) {
    const data = await database.users.findOne({ id });
    return new User(data.name, data.email);
  }
}

View

The View renders HTML for the user. It receives data from the Controller and displays it. No database calls, no business logic, just presentation.

<!-- views/users/profile.ejs -->
<div class="user-profile">
  <h1><%= user.name %></h1>
  <p><%= user.email %></p>
  <a href="/users/<%= user.id %>/edit">Edit profile</a>
</div>

Controller

The Controller is the glue. It receives a request, calls the right Model methods, and hands the result to the right View. Thin controllers are good controllers.

class UserController {
  async getProfile(req, res) {
    const user = await User.findById(req.params.id);
    res.render('users/profile', { user });
  }

  async updateProfile(req, res) {
    const user = await User.findById(req.params.id);
    user.name = req.body.name;
    await user.save();
    res.redirect('/profile');
  }
}
02

How data flows through MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them.

When a user clicks "Edit Profile", here is the chain of events:

1. Browser sends request to /users/42
2. Router matches and calls UserController.getProfile
3. Controller calls User.findById(42)
4. Model queries the database, returns a User object
5. Controller calls res.render('profile', { user })
6. View renders HTML using user data
7. Browser displays the page

Nothing in step 3 knows about HTML. Nothing in step 6 knows about the database. That separation is the whole point.

03

MVCWhat is mvc?Model-View-Controller - a pattern that splits an app into three parts: the Model handles data, the View handles display, and the Controller connects them. across frameworks

FrameworkModelViewController
Express.jsmodels/views/ (.ejs)controllers/
Djangomodels.pytemplates/views.py (confusingly named)
Ruby on Railsmodels/views/ (.erb)controllers/
Laravelapp/Models/resources/views/app/Http/Controllers/
Django names its controllers "views" and its views "templates." It's the same MVC concept with different labels, something that trips up almost everyone the first time.
04

Modern variations

Modern full-stack apps often split the V out entirely. Your backend becomes an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. that returns JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it., and a separate React or Vue frontend handles all the views.

Backend (API):
  Models     ✓
  Controllers ✓
  Views        (returns JSON instead)

Frontend (React):
  Components handle all presentation

This pattern is sometimes called MVVM (Model-View-ViewModel), where React components and state managers like Redux act as the ViewModel layer. The core principle, separate data from presentation, stays the same.

05

The fat controller trap

// Bad: business logic leaking into the controller
app.post('/users', async (req, res) => {
  if (!req.body.email.includes('@')) return res.status(400).json({ error: 'Invalid email' });
  if (req.body.name.length < 2) return res.status(400).json({ error: 'Name too short' });
  const hashed = await bcrypt.hash(req.body.password, 10);
  // ...more logic...
});

// Good: controller stays thin, Model owns the rules
app.post('/users', async (req, res) => {
  const user = new User(req.body);
  await user.save(); // validation happens inside the model
  res.json(user);
});
If your controller is more than 20-30 lines, that's a signal some logic belongs in a Model or a Service layer.
06

Quick reference

PartOwnsDoes NOT own
ModelDB queries, validation, business rulesHTML rendering, request parsing
ViewHTML templates, formattingDatabase calls, business logic
ControllerRequest handling, routing logicBusiness rules, raw SQL
javascript
// Complete MVC example (Express.js)

// models/User.js
class User {
  constructor(data) {
    this.id = data.id;
    this.name = data.name;
    this.email = data.email;
  }

  validate() {
    if (!this.email.includes('@')) {
      throw new Error('Invalid email');
    }
    if (this.name.length < 2) {
      throw new Error('Name too short');
    }
  }

  async save() {
    this.validate();
    // Save to database
    await database.users.insert(this);
  }

  static async findById(id) {
    const data = await database.users.findOne({ id });
    return new User(data);
  }

  static async findAll() {
    const data = await database.users.find();
    return data.map(u => new User(u));
  }
}

module.exports = User;

// controllers/userController.js
const User = require('../models/User');

exports.listUsers = async (req, res) => {
  const users = await User.findAll();
  res.render('users/index', { users });
};

exports.showUser = async (req, res) => {
  const user = await User.findById(req.params.id);
  res.render('users/show', { user });
};

exports.createUser = async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.redirect(`/users/${user.id}`);
  } catch (error) {
    res.render('users/new', { error: error.message });
  }
};

// routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();

router.get('/users', userController.listUsers);
router.get('/users/:id', userController.showUser);
router.post('/users', userController.createUser);

module.exports = router;

// views/users/show.ejs
<!DOCTYPE html>
<html>
<head>
  <title>User Profile</title>
</head>
<body>
  <h1><%= user.name %></h1>
  <p>Email: <%= user.email %></p>
  <a href="/users/<%= user.id %>/edit">Edit</a>
</body>
</html>

// app.js
const express = require('express');
const userRoutes = require('./routes/userRoutes');

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

app.listen(3000);