Layered Architecture

Layered Architecture (слоистая архитектура) — это паттерн организации кода Node.js/Express приложения, при котором код разделяется на чёткие слои: Routes → Controllers → Services → Repository/DAL — с однонаправленными зависимостями между ними.

Зачем нужно

Без структуры вся логика оседает в route-обработчиках: SQL-запросы, бизнес-правила, форматирование ответа. Это делает код нетестируемым и нерасширяемым. Слоистая архитектура разделяет ответственности: каждый слой занимается своим делом, легко тестируется в изоляции и может быть заменён независимо.

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

  • REST API на Express любого масштаба — от pet-проектов до enterprise
  • Основа для чистой архитектуры и Hexagonal Architecture
  • Шаблон для NestJS (контроллеры, провайдеры, модули)
  • Стартовая точка для микросервисов

Основной контент

Слои и их ответственности

HTTP Client
    ↓
┌─────────────┐
│   Routes    │  Объявление маршрутов, подключение middleware
└──────┬──────┘
       ↓
┌─────────────┐
│ Controllers │  Парсинг req, вызов Service, формирование res
└──────┬──────┘
       ↓
┌─────────────┐
│  Services   │  Бизнес-логика, валидация, оркестрация
└──────┬──────┘
       ↓
┌─────────────┐
│ Repository  │  Работа с БД: SQL/Mongoose-запросы
└─────────────┘

Структура файлов

src/
  routes/
    users.js          ← маршруты
  controllers/
    UsersController.js ← обработка запроса/ответа
  services/
    UsersService.js   ← бизнес-логика
  repositories/
    UsersRepository.js ← SQL/ORM запросы
  models/
    User.js           ← схема БД (Mongoose/Prisma)

Repository — работа с БД

// repositories/UsersRepository.js
const { db } = require('../db');

class UsersRepository {
  async findAll {
    return db('users').select('*');
  }

  async findById(id) {
    return db('users').where({ id }).first;
  }

  async create(data) {
    const [id] = await db('users').insert(data);
    return this.findById(id);
  }
}

module.exports = new UsersRepository();

Service — бизнес-логика

// services/UsersService.js
const UsersRepository = require('../repositories/UsersRepository');
const { ValidationError, NotFoundError } = require('../errors');
const bcrypt = require('bcrypt');

class UsersService {
  async create({ name, email, password }) {
    const existing = await UsersRepository.findByEmail(email);
    if (existing) throw new ValidationError('Email already in use');

    const hashedPassword = await bcrypt.hash(password, 10);
    return UsersRepository.create({ name, email, password: hashedPassword });
  }

  async getById(id) {
    const user = await UsersRepository.findById(id);
    if (!user) throw new NotFoundError('User');
    return user;
  }
}

module.exports = new UsersService();

Controller — обработка HTTP

// controllers/UsersController.js
const UsersService = require('../services/UsersService');

class UsersController {
  async getAll(req, res, next) {
    try {
      const users = await UsersService.getAll;
      res.json(users);
    } catch (err) {
      next(err);
    }
  }

  async create(req, res, next) {
    try {
      const user = await UsersService.create(req.body);
      res.status(201).json(user);
    } catch (err) {
      next(err);
    }
  }
}

module.exports = new UsersController();

Routes — объявление маршрутов

// routes/users.js
const express = require('express');
const router = express.Router;
const UsersController = require('../controllers/UsersController');
const { authMiddleware } = require('../middleware/auth');

router.get('/', authMiddleware, UsersController.getAll.bind(UsersController));
router.post('/', UsersController.create.bind(UsersController));

module.exports = router;

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

  • Пропустить слой Service — писать SQL прямо в контроллере: бизнес-логика не тестируется изолированно
  • Нарушать однонаправленность — Repository не должен знать о Controller или HTTP; Service не должен знать о req/res
  • Один огромный сервис — разбивать по доменам (UsersService, OrdersService), а не один AppService
  • Singleton-репозитории с состоянием — опасно при многопоточности (Worker Threads); лучше классы без состояния

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

Ресурсы


🎓 Источник: Слои, связанность и связность кода

  • 📅 2018-10-23 · YouTube · [Marp](../../Documents/TimurShemsedinov/2018-10-23 — Слои, связанность и связность кода в JavaScript (A3RpwNlVeyY).md)
  • Тезисы:
    • Connascence: степень связи между компонентами; чем «ближе» — тем хуже масштабируется
    • Слои движутся в одном направлении: api → domain → db (никогда обратно)
    • High cohesion внутри слоя, low coupling между слоями — фундамент GRASP
    • Конфиг — отдельный слой; не разбрасывай константы по бизнес-логике

🎓 Источник: Архитектурные принципы из курса Node.js 2024

  • 📅 2023-12-06 · YouTube
  • Тезисы: актуальная сборка архитектурных принципов: слои, DDD, DI, framework-agnostic подход

🎓 Источник: Node JS 2022-2023 практический курс

  • 📅 2022-08-29 · YouTube
  • Тезисы:
    • Фреймворк-агностик подход — бизнес-логика не должна знать про Express/Nest
    • Транспорт- и протокол-агностик: одна функция работает по HTTP, WebSocket, IPC
    • DDD, SOLID, GRASP, паттерны GoF
    • Шаблон приложения с готовой архитектурой

🎓 Источник: Модули, слои, структура проекта

  • 📅 2018-10-02 · YouTube
  • Тезисы:
    • Подмодули в папке lib, композиция через Object.assign в цикле по именам
    • Логирование — отдельный модуль, композиция модулей в одном месте приложения
    • ESM (.mjs) и CommonJS несовместимы — не смешивать
    • Через require.resolve и require.cache можно перезагружать модули в рантайме

🎓 Источник: Структура приложения важнее архитектуры

  • 📅 2026-05-16 · YouTube
  • Тезисы:
    • Альтернативная позиция: «Структура приложения важнее архитектуры». На архитектуру влияет не структурный код, а низкоуровневые характеристики: идемпотентность, состояние, ссылки.
    • Структура приложения — это система модульности и приёмы её использования.
    • Структура данных в основании модуля решает, можно ли размазать модуль на потоки/машины/дата-центры.

🎓 Источник: Архитектурный подход к программированию

  • 📅 2018-09-25 · YouTube
  • Тезисы:
    • Архитектура нужна когда появляется сложность — за пределы одного часа работы одного программиста
    • Давать имена — одна из важнейших и сложнейших архитектурных задач
    • Архитектура = принятие решений: организация данных, обработка ошибок, безопасность