Приведи пример монолитной архитектуры в Node.js приложении

«Приведи пример монолитной архитектуры в Node.js приложении» — вопрос из категории Архитектура, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В моем последнем проекте мы использовали монолитную архитектуру на Express.js, где все компоненты были в одном репозитории и процессе:

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

monolith-app/
├── src/
│   ├── controllers/     # Обработчики маршрутов
│   ├── models/          # Модели данных (Mongoose/Squelize)
│   ├── routes/          # Определение маршрутов
│   ├── middleware/      # Промежуточное ПО
│   ├── services/        # Бизнес-логика
│   ├── utils/          # Вспомогательные функции
│   └── app.js          # Основной файл приложения
├── package.json
└── .env

Пример app.js:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());

// Подключение к MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// Маршруты
app.use('/api/users', require('./src/routes/userRoutes'));
app.use('/api/products', require('./src/routes/productRoutes'));
app.use('/api/orders', require('./src/routes/orderRoutes'));

// Пример маршрута с полным стеком в одном месте
app.get('/api/dashboard', async (req, res) => {
  try {
    // 1. Получаем данные из БД
    const userCount = await User.countDocuments();
    const productCount = await Product.countDocuments();
    const recentOrders = await Order.find()
      .sort({ createdAt: -1 })
      .limit(10)
      .populate('user');

    // 2. Применяем бизнес-логику
    const growthRate = await calculateGrowthRate();

    // 3. Формируем ответ
    res.json({
      users: userCount,
      products: productCount,
      recentOrders,
      growthRate,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error('Dashboard error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Запуск сервера
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Monolithic app running on port ${PORT}`);
});

Преимущества, которые я наблюдал:

  • Быстрый старт — один репозиторий, одна кодовая база
  • Простая отладка — все логи в одном процессе, стектрейсы полные
  • Сквозная типизация — если используется TypeScript, типы доступны везде
  • Общие ресурсы — одно подключение к БД, общий кэш в памяти

Проблемы, с которыми столкнулся:

  • Разрастание кодовой базы — через год было 50+ маршрутов и 30+ моделей
  • Сложность деплоя — обновление одной функции требовало перезапуска всего приложения
  • Масштабирование — приходилось масштабировать весь монолит, даже если нагрузка была только на один модуль

Мы начали выделять модули в отдельные сервисы, когда ежедневная нагрузка превысила 100k запросов.