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 — промежуточный слой для сложной логики и тестируемости.