Express.jsMiddleTechnical

Что такое паттерн MVC и как он применяется в Express?

MVC делит Express-приложение на Model (данные/БД), View (шаблоны или JSON), Controller (оркестрация). Роуты только регистрируют пути и вызывают методы контроллера.

Паттерн MVC в Express.js

MVC (Model–View–Controller) — архитектурный паттерн, разделяющий приложение на три слоя с чёткими обязанностями:

  • Model — бизнес-логика и работа с данными (запросы к БД, валидация, трансформация).
  • View — представление данных пользователю (HTML-шаблоны или JSON для API).
  • Controller — оркестратор: принимает запрос, вызывает модели, передаёт данные во view.

Структура проекта

src/
  models/
    user.model.js      # SQL/ORM-запросы
    job.model.js
  controllers/
    user.controller.js # обработчики запросов
    job.controller.js
  routes/
    user.routes.js     # только маршрутизация
    job.routes.js
  views/               # шаблоны (или не нужны для REST API)
    jobs/list.pug
  services/            # опциональный слой бизнес-логики
    job.service.js
  app.js

Пример реализации

// models/job.model.js — работа с данными
import { pool } from '../db.js';

export const JobModel = {
  async findAll({ limit = 20, offset = 0 } = {}) {
    const { rows } = await pool.query(
      'SELECT id, title, company, salary FROM jobs ORDER BY created_at DESC LIMIT $1 OFFSET $2',
      [limit, offset]
    );
    return rows;
  },

  async findById(id) {
    const { rows } = await pool.query(
      'SELECT * FROM jobs WHERE id = $1',
      [id]
    );
    return rows[0] ?? null;
  },

  async create({ title, company, salary, description }) {
    const { rows } = await pool.query(
      'INSERT INTO jobs (title, company, salary, description) VALUES ($1, $2, $3, $4) RETURNING *',
      [title, company, salary, description]
    );
    return rows[0];
  },
};
// controllers/job.controller.js — оркестрация
import { JobModel } from '../models/job.model.js';

export const JobController = {
  async list(req, res, next) {
    try {
      const { page = 1, limit = 20 } = req.query;
      const offset = (Number(page) - 1) * Number(limit);
      const jobs = await JobModel.findAll({ limit: Number(limit), offset });
      res.json({ data: jobs, page: Number(page) });
    } catch (err) {
      next(err);
    }
  },

  async detail(req, res, next) {
    try {
      const job = await JobModel.findById(req.params.id);
      if (!job) return res.status(404).json({ error: 'Job not found' });
      res.json(job);
    } catch (err) {
      next(err);
    }
  },

  async create(req, res, next) {
    try {
      const job = await JobModel.create(req.body);
      res.status(201).json(job);
    } catch (err) {
      next(err);
    }
  },
};
// routes/job.routes.js — только маршрутизация
import { Router } from 'express';
import { JobController } from '../controllers/job.controller.js';
import { authenticate } from '../middleware/auth.js';

const router = Router();

router.get('/', JobController.list);
router.get('/:id', JobController.detail);
router.post('/', authenticate, JobController.create);

export default router;

// app.js
import jobsRouter from './routes/job.routes.js';
app.use('/api/jobs', jobsRouter);

Когда добавлять слой Service

Если бизнес-логика сложная (отправка email после создания вакансии, взаимодействие с несколькими моделями), её выносят в services/. Controller вызывает Service, а не Model напрямую. Это сохраняет контроллеры тонкими и упрощает тестирование.

Подводные камни

  • Толстые контроллеры — если в контроллере появляется бизнес-логика (расчёты, агрегации), её нужно выносить в модели или сервисы.
  • Прямые SQL-запросы в контроллерах — нарушает MVC: модель должна владеть всеми операциями с данными.
  • Смешение слоёв маршрутизации и контроллеров — писать обработчик прямо в routes-файле удобно для прототипа, но создаёт неподдерживаемый код.
  • Отсутствие обработки ошибок в контроллере — каждый async-метод должен оборачивать await в try/catch и вызывать next(err).
  • Жёсткая связь модели с конкретной СУБД — лучше абстрагировать интерфейс модели, чтобы легко подменять источник данных в тестах.
  • View как HTML-шаблон vs JSON — в REST API роль View играет res.json(); не нужно добавлять шаблонизатор только ради MVC.

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API Express.js.
  • Не объясняет, где в lifecycle находится MVC в Express.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить MVC в Express на примере кода.
  • Называет ключевые API: controller, service, model.
  • Использует точные API Express.js, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics