MVC на Node.js

MVC (Model-View-Controller) — архитектурный паттерн, разделяющий приложение на три слоя: Model (данные и бизнес-логика), View (отображение) и Controller (обработка запросов и оркестрация).

Зачем нужно

MVC — классический паттерн для серверных веб-приложений: Express, NestJS, Rails, Laravel — все следуют его принципам. Знание MVC помогает ориентироваться в любом бэкенд-проекте на Node.js, понимать структуру файлов и добавлять новый функционал в правильное место. Разделение ответственности делает код тестируемым: Controller без слоя данных, Model без HTTP-зависимостей.

Где используется

  • Express.js API (de facto стандарт структуры)
  • NestJS — встроенная MVC-структура с контроллерами и сервисами
  • Fullstack фреймворки (Adonis.js, Sails.js)
  • REST API backend для SPA-фронтенда

MVC на Express.js

src/
  models/
    User.js         ← Model: схема + методы работы с данными
    Product.js
  controllers/
    userController.js  ← Controller: обработка HTTP-запросов
    productController.js
  routes/
    userRoutes.js    ← Маршрутизация → Controller
  services/
    userService.js   ← Бизнес-логика (опционально, часть Model)
  views/             ← View: шаблоны (для SSR) или не нужны для API
    index.ejs
  app.js             ← Настройка Express

Model: схема данных (Mongoose)

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now() },
});

// Бизнес-логика в Model (хэширование пароля)
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next;
  this.password = await bcrypt.hash(this.password, 10);
  next;
});

userSchema.methods.comparePassword = function(candidate) {
  return bcrypt.compare(candidate, this.password);
};

module.exports = mongoose.model('User', userSchema);

Controller: обработка запросов

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

// Controller тонкий — только HTTP-слой
const userController = {
  // GET /users
  async getAll(req, res) {
    try {
      const users = await userService.getAllUsers;
      res.json({ success: true, data: users });
    } catch (error) {
      res.status(500).json({ success: false, message: error.message });
    }
  },

  // GET /users/:id
  async getById(req, res) {
    try {
      const user = await userService.getUserById(req.params.id);
      if (!user) return res.status(404).json({ message: 'Пользователь не найден' });
      res.json({ success: true, data: user });
    } catch (error) {
      res.status(500).json({ success: false, message: error.message });
    }
  },

  // POST /users
  async create(req, res) {
    try {
      const { name, email, password } = req.body;
      const user = await userService.createUser({ name, email, password });
      res.status(201).json({ success: true, data: user });
    } catch (error) {
      res.status(400).json({ success: false, message: error.message });
    }
  },
};

module.exports = userController;

Routes: соединяет URL с Controller

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

router.get('/', authMiddleware, userController.getAll);
router.get('/:id', authMiddleware, userController.getById);
router.post('/', userController.create);

module.exports = router;

// app.js
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);

Частые ошибки

  • Fat Controller — бизнес-логика, SQL-запросы и HTTP-ответы в одном методе контроллера; выносите логику в Service/Model.
  • Fat Model — Model знает о HTTP-ответах (res.json(...) внутри модели); Model должна возвращать данные/ошибки, не отвечать напрямую.
  • Нет Service слоя — Controller напрямую обращается к Model; Service — промежуточный слой для сложной логики и тестируемости.

Связанные темы

Ресурсы