Структураванне служб Node.js за дапамою модуляў, адмованых на домэне, і чыстых шароў
Выучыце, як арганізаваць базу коду Node.js у склад частакоў, заснованых на домэнах, застаўляць строгую 3-ярусную архітектуру і выклікаць спяльныя функцыі через чыстыя публічныя API.
Калі вы запускаеце новыя сервісы на Node.js, працэс розвитку шырокаўаецца быстра. Фрамворкі такія як Express, Fastify і NestJS дазваляюць запусціць точкі входу на протаколах REST або GraphQL за кальканы час. Але калі команды растуць, а логіка бізнесу стае ўсё болей складной, у такіх кодавых базах часта з’яўляецца вядомая проблема: яны ператвараюця на заплутаны, важкі для розумення хаос, які часта называюць «спагеты-бэкендам».
У этай статыце прадстаўлены чатыры аспекта практык викорыстоўвання Node.js на рэвэню-рангу, сфокусаваныя на стварэнні архітектурных адносоў, якія зможуць выстояць пад тым натискам, які стварае растучая команда. Вы пазнаёсце, як арганізаваць код за прынцыпамі бізнес-домэнаў, як зберагчы чыстую разліку межаў між слоямі, а таксама як стварыць спяльныя модулі, якія не стануць трыбам для команды.
1. Структуразаванне па бізнес-компонентах (організацыя на адной з домэнаў)
Адзін з частаўых памылак у проектах Node.js — гэта арганізаванне кодавой базы выключна за прынцыпам тэхнічных ролей на верхнім роўні папкі src.
Анті-шаблон: тэхнічна стратыфікацыя на коране
❌ AVOID: Layered by technical type
src/
├── controllers/
│ ├── userController.js
│ ├── orderController.js
│ └── paymentController.js
├── models/
│ ├── userModel.js
│ ├── orderModel.js
│ └── paymentModel.js
├── services/
│ ├── userService.js
│ ├── orderService.js
│ └── paymentService.js
└── routes/
├── userRoutes.js
├── orderRoutes.js
└── paymentRoutes.js
Чаму тэхнічная стратыфікацыя не функцыонуе у масштабных проектах
- Недастатковая локальнасць: Стварэнне адной-ўсёй функцыялкі — напрыклад, процесу „Вернуць грошы за замовленне“ — вымагае переходу між чатырма або болей некалязанымі папкамі.
- Нечыстыя межы: Усе тэхнічныя элементы знаходзяцца разам, таму інжынеры вынужданы працаваць безпосередна з рознымі сервісамі та моделямі, што спрыяе виникненню цыркулярных залежнасцей та жорсткага з’ѐеднання.
- Збільшэнне когнітываўкі: Калі колькасць модэляў перасягае 50, монолітны каталог
controllers/абоmodels/стае справжна складным для навігацыі.
Рашэння: модульныя компаненты домэна
Лепшы падход — арганізавацыя ў рамках бізнес-возможнасцей — тое, што Domain-Driven Design называе Bounded Contexts. Кожны компонент становіцца самодастатнім доменам, які об’едначае своія кантролеры, сервісы, репазітарыі і моделі домена.
✅ PREFER: Component-based architecture
src/
├── components/
│ ├── users/
│ │ ├── users.controller.js
│ │ ├── users.service.js
│ │ ├── users.repository.js
│ │ └── users.routes.js
│ ├── orders/
│ │ ├── orders.controller.js
│ │ ├── orders.service.js
│ │ ├── orders.repository.js
│ │ └── orders.routes.js
│ └── payments/
│ ├── payments.controller.js
│ ├── payments.service.js
│ └── payments.repository.js
└── shared/
├── logger/
└── database/
Прыямнікі:
- Самадастаточнае выкананне: Усё, што зв’язана з
orders, застаецца ў аднам папкі. - Гатовнасць да мікросервісаў: Якщо компонент
paymentsстане занадта вялікім або складным, выкарыстоўваць як самастоятельны мікросервіс стане набагато проста, адколі яго залежнасці вже ізольаваныя ад рэшты прыкладнага програму.
2. Забезпечыць строгую трываровую архітектуру ўнутранік компонентаў
У кожным бізнес-компоненте неабходна чыстая разліка межаў трохоў:
┌─────────────────────────────────────────────────────────┐
│ 1. Entry Point / Transport Layer │
│ (Controllers, Event Subscribers, Route Handlers) │
└───────────────────────────┬─────────────────────────────┘
│ Calls with plain DTOs
▼
┌─────────────────────────────────────────────────────────┐
│ 2. Domain / Business Layer │
│ (Services, Business Logic, Validation Rules) │
└───────────────────────────┬─────────────────────────────┘
│ Calls repository methods
▼
┌─────────────────────────────────────────────────────────┐
│ 3. Data Access Layer │
│ (Repositories, ORMs, Database Queries) │
└─────────────────────────────────────────────────────────┘
Золатая правіла: не пускайце веб-об’екты ў логіку домэны
Службы бізнесу і рэпазітары ніколі не должны прымаць об’екты, спецыфічныя для фрэймворку — ні Express req, ні res, ні інстанцыя запиту Fastify.
Неправільны спосаб: вытэканне об’ектаў HTTP-транспорту
// orders.service.js
async function createOrderService(req, res) {
// BAD: Service knows about HTTP headers, status codes, and req.body
const userId = req.headers['x-user-id'];
const orderData = req.body;
if (!orderData.items || orderData.items.length === 0) {
return res.status(400).json({ error: "Cart cannot be empty" });
}
const newOrder = await db.orders.insert({ userId, ...orderData });
return res.status(201).json(newOrder);
}
Чаму гэта ламае:
createOrderServiceстановіцца непрыдатным параду HTTP-контэксту — яго нельга вызваць з скрыпты CLI, кансумэру Kafka чы РабітМК, або з задання cron, адколі ніхто з іх не прадаеreqчыres.- Тэсты тепер павінны імітаваць об’екты запиту та адпаведзі HTTP у замяну на перадачу звычных значэнняў JavaScript.
Правільны спосаб: роз’ўязаны шар служб
// orders.controller.js (Transport Layer)
const orderService = require('./orders.service');
async function handleCreateOrder(req, res, next) {
try {
// 1. Extract values from web context
const userId = req.headers['x-user-id'];
const { items, shippingAddress } = req.body;
// 2. Call domain service with pure primitives/DTOs
const order = await orderService.createOrder({
userId,
items,
shippingAddress
});
// 3. Format HTTP response
return res.status(201).json({ status: 'success', data: order });
} catch (err) {
next(err); // Defer error handling to central middleware
}
}
// orders.service.js (Domain Layer)
const orderRepo = require('./orders.repository');
async function createOrder({ userId, items, shippingAddress }) {
// 1. Pure business logic validation
if (!items || items.length === 0) {
throw new ValidationError('Order must contain at least one item.');
}
// 2. Business calculation
const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// 3. Persist via repository
const createdOrder = await orderRepo.saveOrder({
userId,
items,
shippingAddress,
totalAmount,
status: 'PENDING'
});
return createdOrder;
}
3. Модулярэзаванне інструментаў та адбавленне чыстых публічных API
Калі програма расте, багатыя компаненты пачаткуюць абавязкова выкарыстоўваць спакульнаныя функцыі, такія як логаванне, аутэнтыкацыя, калектары для з’ўязку з базамі дадзеных і кліенты для зовнішніх API. У замест на тое, каб кожны корыстувач могаў безпасяродна працаваць з глыбокімі внутранімі шляхамі файлоў, такія спакульнаныя інструменты трэба запакаваць у внутраніяе модулі з чыста адзначаным публічным входам.
Апасцерк глыбокага імпорту
// ❌ BAD: Tightly coupled to internal folder structures
const { formatLog } = require("../../shared/logger/utils/formatters/textFormatter.js");
Якщо каманда, якая адпаведае за логгер, пазней вырашыць пераранжаваць структуру своіх внутраніх папак, кожны корыстувач, які імпортаваў матэрыял з таго глыбокага шляху, негайна паспяшыцца.
Рашэнне А: Экспорт чераз index.js (CommonJS)
Створыце адны файл-вхід, які занова экспортуе толькі тыя элементы, якія патрэбны для зовнішняй работы, а ўсё інше захаваеце.
// shared/logger/index.js
const { logger } = require('./loggerCore');
const { auditLog } = require('./auditLogger');
// Expose ONLY public functions
module.exports = {
logger,
auditLog
};
Тады корыстувачы могу імпортаваць матэрыял через гэты чысты інтэрфейс, у замест на тое, каб працаваць з внутранімі элементамі:
// ✅ GOOD: Clean import via public interface
const { logger } = require('../../shared/logger');
Рашэння B: Экспорт пакетаў з аднойчынным скарыстоўванням коду (ESM) (Node.js Workspaces / Савэцкі ESM)
Якщо вы працуеце з савэцкім кодам Node.js ESM або пакетамі ў манорепа, выкорыстайце поле exports у файле package.json, каб чытка абмежыць, якія файлы можна імпортаваць званутра з пакета.
// shared/logger/package.json
{
"name": "@my-app/logger",
"version": "1.0.0",
"main": "./src/index.js",
"exports": {
".": "./src/index.js"
}
}
З такой налаштованасцю будзе абараняцца будзь-якае падзея імпорту прыватнага шляху, такога як @my-app/logger/src/internal/formatter.js, з адпаведным крэтывацкім выключэнням, што стварае чыстую межу для вашай команды і запобегае випадковаму зв’язку з деталямі рэалізацыі.
Чакліст архітэктуры для часткі 1
Перш чым перайсці да карэткавання памылак і асінхронных робочых процэсаў, выкорыстайце этот чакліст, каб пераканацца, што ваш кодбаза адпавядае вышэўзгаданым прынцыпам:
- Архітектура домэна: Чы распаўзаны код па функцыйных складовых (
orders,users) у замест на загальныя тэхнічныя папкі (controllers,models)? - Чыстыя бізнес-сервісы: Чы функцыі вашых сервісаў не выкарыстоваюць параметры рэйя прынесення, такія як
req,resчыnext? - Ясныя інтэрфейсы: Чы спільныя модулі маюць цэлесапраўедзеную, абмежаную публічную структуру, чы то чераз файл
index.js, чы чераз экспорты зpackage.json? - Правільны прайом дадзеных: Чы дадзеныя пераводзяцца строго ад контралера да сервіса, а затым да рэпазітарыя, без таго, каб нижэйшыя слойвы моглі імпортуваць дадзеныя з вышэйшых?
Супаўзеленыя матэрыялы
- Дзеянне API на Node.js з слоямі: ад «тэплых» кантролераў да чыстай архітэктуры — Дазвольце вам дазнацца, як перархітектураваць API на Node.js у слоі кантролераў, сэрвісаў і аблекчыкаў дадзенняў, каб усунуць заплутаную бізнес-логіку, нэўнацоўныя памылкі і проблемы з масштабаваннем.
- Шэсць правілаў DDD для структуравання домену ў прыкладных програмах NestJS — Дазвольце вам дазнацца шэсць практычных правілаў дзеяння, адвядзенага да домену, для арганізавання модуляў, энтытаў і запускаў у NestJS, каб функцыі заставаліся ізольаванымі та прыемнымі для адтрымкі.