Привет, Хабр! Я Борис Ермаченко, фронтенд-разработчик сервисов для физических лиц ПСБ. В этой статье рассмотрим, как с помощью ESLint построить архитектуру в проекте, и поговорим про несколько подходов.
Также прикладываю бонус — демо-проект, где можно экспериментировать и пробовать все подходы.

Введение: архитектура и ее границы
Архитектура проекта определяет его эволюцию: насколько легко добавлять новые фичи, рефакторить код и поддерживать систему в долгосрочной перспективе. Хорошая архитектура — это не просто удобное разбиение на файлы и папки, а четкие границы между слоями.
Для чего все это нужно
Чтобы проще разобраться, рассмотрим кейсы, которые могут произойти в работе над проектом:
1. Переход от монолита к микрофронтендам
Кейс: Компания разрабатывала монолитное фронтенд-приложение, но со временем требования бизнеса изменились. Команды стали работать автономно, развивая разные направления продукта, и возникла необходимость в переходе на микрофронты.
Проблема: В коде отсутствуют четкие границы между фичами. Одни модули напрямую импортируют другие, бизнес-логика размыта, и рефакторинг становится сложным.
Решение: С помощью eslint-plugin-boundaries
можно внедрить строгую модульную архитектуру, которая упростит разбиение проекта. Что это за плагин и как с ним работать, рассмотрим чуть позже в этой статье.
2. Запрет на использование устаревших библиотек и зависимостей
Кейс: Проект активно развивается, но в коде до сих пор используются устаревшие библиотеки moment.js и lodash, хотя уже принято решение перейти на date-fns и Ramda.
Решение: Использовать no-restricted-imports
, чтобы запретить импорт устаревших зависимостей. Что это за правило, узнаем ниже.
3. Запрет на импорт UI-компонентов в ядро проекта
Кейс: В проекте есть модуль shared/ui, в котором лежат переиспользуемые кнопки, инпуты и другие UI-компоненты. Однако один из разработчиков случайно заимпортил кнопку Button из shared/ui в core, где находятся бизнес-логика и API-клиенты.
Это создает проблемы:
core становится зависимым от UI, логика и представление перемешиваются, что усложняет поддержку;
возможны циклические зависимости, если shared/ui тоже импортирует что-то из core.
Решение: Запретить импорт UI-компонентов (shared/ui) в core с помощью no-restricted-imports
(еще немного, и узнаем про это правило).
Вывод по кейсам
Возможно, вы узнали в представленных ситуациях свои проблемы. А, может быть, в вашем проекте другая боль, связанная с зависимостями между модулями?
Перед тем, как перейдем к разобру ESLint-подходов, кратко рассмотрим принципы чистой архитектуры. Эту часть можно пропустить, если у вас уже есть опыт в архитектурных вопросах.
Роберт Мартин в своей книге «Чистая архитектура» описывает несколько ключевых принципов построения архитектуры, которые актуальны и для фронтенд-разработки:
Разделение на уровни (layers) — код должен быть организован в слои. Например, бизнес-логика, UI, инфраструктура. Причем зависимости должны идти только сверху вниз. Это предотвращает хаотичное переплетение кода.
Зависимости должны быть направлены внутрь (Dependency Rule) — низкоуровневые модули (HTTP-запросы, работа с DOM) не должны навязывать свою структуру бизнес-логике. Например, React-компоненты не должны зависеть от специфики API.
Изоляция от конкретных деталей (Details should depend on abstractions) — архитектура не должна зависеть от деталей реализации, таких, как конкретные библиотеки или фреймворки. Это упрощает замену технологий и рефакторинг. Например, React-компонент напрямую не зависит от конкретной библиотеки, с помощью которой посылаются запросы на бэк.
Подробнее про чистую архитектуру можно прочитать здесь: Блог Дядюшки Боба.

Где здесь ESLint?
Какие бы принципы вы ни выбрали для своей архитектуры, их легко нарушить, особенно в больших проектах. Например, фича-модуль может начать напрямую использовать низкоуровневый API вместо того, чтобы работать через сервисный слой. Или утилиты из shared начнут зависеть от бизнес-логики.
Архитектура — это про связи и зависимости между частями системы, что выражается в коде через импорты.
Преимущество ESLint-подходов — возможность кастомизировать правила под специфику конкретного проекта и архитектурной методологии, которую выбрала команда. И вне зависимости от того, что вы используете: модульную архитектуру, слоистую (layered) архитектуру, FSD, микрофронты.
Вручную следить за этими зависимостями сложно, и ESLint позволяет автоматизировать этот процесс. Рассмотрим три подхода, которые помогут явно обозначить границы между модулями и сделать архитектуру проекта более устойчивой.
import/no-restricted-paths
no-restricted-imports
eslint-plugin-boundaries
1. eslint-plugin-import (import/no-restricted-paths)
Этот подход позволяет запрещать импорт из определенных директорий в зависимости от расположения файла (то есть запрещает импорт чего-то в том случае, если пытаемся заимпортить из определенного места). Он полезен для ограничения доступа между слоями архитектуры, например, запрещая фичам напрямую обращаться к низкоуровневым модулям.
Важное замечание — установите и настройте eslint-import-resolver-typescript
для работы eslint-plugin-import
.
Пример
Допустим, у нас есть такая структура:
/src
├── core/
│ ├── api/
│ ├── services/
│ └── utils/
├── features/
│ ├── dashboard/
│ └── settings/
├── shared/
└── index.ts
Добавим для нашего проекта правило — фича-модулям запрещено напрямую использовать API
из core/api
, вместо этого они должны работать через сервисы core/services
.

Конфигурация ESLint:
{
"rules": {
"import/no-restricted-paths": [
"error",
{
"zones": [
{
“target”: “./src/features/**/*”,
“from”: “./src/core/api”,
"message": "Фичи должны использовать core/api только через core/services"
}
]
}
]
}
}
Если разработчик попытается импортировать core/api
в features/dashboard/index.ts
, ESLint выдаст ошибку. Попробуйте в демо-проекте.
2. no-restricted-imports
Это правило уже встроено в ESLint и не требует установки плагина.
Подход запрещает конкретные импорты независимо от пути текущего файла. Он удобен для более точечного контроля, например, если нужно ограничить использование сторонних библиотек или отдельных модулей внутри проекта.
Пример

Запретим использовать date-fns
, обязывая импортировать утилиты из shared/utils/date.ts
:
{
"rules": {
"no-restricted-imports": [
"error",
{
paths: [
{
name: 'date-fns',
message: 'Используйте shared/utils/date.ts вместо date-fns',
},
],
}
]
}
}
Если в коде появится import { format } from "date-fns"
, ESLint выдаст ошибку, заставляя разработчика использовать наш обернутый модуль.
Есть и более интересные варианты использования, напрямую влияющие на архитектуру. Комбинируя свойства ignores
и patterns
, можно расширить область применения этого правила, например, запретить импорт api
куда угодно, кроме services
.
export default [
{
ignores: ['src/core/services/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/core/api/**'],
message: 'api может быть заимпортировано только в service'
},
]
}
]
},
},
]
Подробнее и больше примеров — в документации.
Попробуйте поэксперементировать в демо-проекте.
3. eslint-plugin-boundaries
Итак, мы рассмотрели два правила, которые, как кажется, могут решить все наши запросы. Но представьте, что мы хотим добавить следующее ограничение — любая фича не должна импортировать код из других фичей. С помощью предыдущих подходов это правило достигается неудобно.
На помощь приходит плагин eslint-plugin-boundaries
, он позволяет контролировать связи между модулями через декларативные описания их типов и дает больше возможностей, чем no-restricted-imports
и import/no-restricted-paths
, так как поддерживает концепцию слоев.

Пример
Добавим в .eslintrc.js
конфигурацию плагина:
{
"plugins": ["boundaries"],
"rules": {
"boundaries/element-types": [
2,
{
"default": "disallow",
"rules": [
{ "from": "feature", "allow": ["shared", "core"] },
{ "from": "core", "allow": ["shared"] },
{ "from": "shared", "allow": [] }
]
}
]
},
"settings": {
"boundaries/elements": [
{ "type": "feature", "pattern": "src/features/*" },
{ "type": "core", "pattern": "src/core/*" },
{ "type": "shared", "pattern": "src/shared/*" }
]
}
}
Теперь feature
-модули могут зависеть от core
и shared
, но shared
остается изолированным. Если кто-то попробует импортировать feature
в core
, ESLint выдаст ошибку. Поэксперементировать можно здесь — демо-проект.
Сравнение подходов
Подход | Гибкость | Контролируемые границы |
import/no-restricted-paths | Средняя | Глобальные запреты на уровне директорий |
no-restricted-imports | Низкая | Запрет конкретных импортов |
eslint-plugin-boundaries | Высокая | Архитектурные слои, контроль |
Заключение
Мы рассмотрели и сравнили три подхода для контроля архитектуры в проекте:
import/no-restricted-paths
— хороший выбор, если нужно запрещать импорты на уровне директорий, но он не учитывает концепцию слоев.no-restricted-imports
— удобен для точечных запретов, например, если нужно ограничить импорт сторонних библиотек или отдельных модулей. Также можно настроить правило для папок черезpattern
.eslint-plugin-boundaries
— самый мощный вариант, так как позволяет явно описывать архитектурные слои и следить за их границами.
В реальном проекте можно комбинировать эти подходы. Например, использовать no-restricted-imports
для запрета сторонних библиотек, import/no-restricted-paths
для ограничений между директориями, а eslint-plugin-boundaries
для контроля архитектурных слоев (попробуйте все подходы на демо-проекте).
Подведем итог. Использование правил ESLint поможет:
начать декомпозицию монолитного приложения на микрофронты
следить за связями между модулями в проекте согласно выбранной архитектуре
запретить использование deprecated кода
Спасибо за внимание! Делитесь в комментариях библиотеками, которыми вы пользуетесь для внедрения архитектуры в проект!