1. Зачем нужны services/, utils/ и models/
Кратко о проблеме
Когда ваше приложение становится больше, контроллеры начинают напоминать швейцарский нож: тут и бизнес-логика, и работа с базой, и валидация, и ещё с десяток разных задач. Это делает код сложным для тестирования, переиспользования и поддержки.
Решение: разделить ответственность между разными слоями и файлами. Каждый слой отвечает только за свою задачу. Это — основа принципа Single Responsibility (одна ответственность), который любят не только программисты, но и их будущие коллеги (особенно те, кто будет поддерживать ваш код).
services/
Service — это слой бизнес-логики. Сервисы отвечают за обработку данных, выполнение бизнес-правил, работу с внешними системами (например, с базой данных или сторонними API). Контроллер вызывает сервис, а сервис делает всю "грязную работу".
utils/
Utils (utility functions) — это вспомогательные функции, которые не зависят от бизнес-логики и не знают ничего о структуре приложения. Это могут быть функции форматирования дат, генераторы случайных строк, функции для проверки email и прочие "мелочи жизни".
models/
Models — это слой структуры данных. Обычно здесь описываются схемы объектов, которые мы храним или передаём (например, структура задачи, пользователя, заказа). В случае работы с базой данных — это схемы для ORM (например, Mongoose для MongoDB) или простые классы/объекты, описывающие ваши данные.
2. services/: бизнес-логика вне контроллеров
Пример из жизни
Допустим, у нас есть API для списка задач (todo-list). Сейчас контроллер выглядит так:
// controllers/todoController.js
const todos = []; // временное хранилище
exports.getAllTodos = (req, res) => {
res.json(todos);
};
exports.createTodo = (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTodo = { id: Date.now(), title, completed: false };
todos.push(newTodo);
res.status(201).json(newTodo);
};
Всё работает, но если нам вдруг потребуется:
- Добавить сложную валидацию,
- Сохранять задачи в базе данных,
- Логировать действия,
- Работать с внешними API,
...наш контроллер быстро превратится в "кашу".
Переносим бизнес-логику в сервис
Создадим папку services/ и файл todoService.js:
// services/todoService.js
const todos = []; // временное хранилище
function getAllTodos() {
return todos;
}
function createTodo(title) {
if (!title || typeof title !== 'string' || !title.trim()) {
throw new Error('Некорректный заголовок задачи');
}
const newTodo = { id: Date.now(), title: title.trim(), completed: false };
todos.push(newTodo);
return newTodo;
}
module.exports = {
getAllTodos,
createTodo,
};
Теперь контроллер становится очень простым:
// controllers/todoController.js
const todoService = require('../services/todoService');
exports.getAllTodos = (req, res) => {
res.json(todoService.getAllTodos());
};
exports.createTodo = (req, res) => {
try {
const newTodo = todoService.createTodo(req.body.title);
res.status(201).json(newTodo);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
В чём профит?
- Вся логика создания и проверки задачи теперь живёт в сервисе.
- Контроллер только принимает запрос, вызывает сервис и отправляет ответ.
- Если завтра мы захотим сохранять задачи в базу данных — меняем только сервис, контроллеры не трогаем.
Когда сервисы особенно полезны
- Когда одна и та же бизнес-логика нужна в нескольких местах (например, создание пользователя при регистрации и при импорте из CSV).
- Когда нужно тестировать бизнес-логику отдельно от Express (юнит-тесты).
- Когда проект становится большим, и бизнес-правила усложняются.
3. utils/: вспомогательные функции
Пример: генератор случайных ID
Иногда в приложении нужны функции, которые не относятся к бизнес-логике, но используются в разных местах.
Создадим файл utils/randomId.js:
// utils/randomId.js
function generateRandomId(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
module.exports = generateRandomId;
Теперь мы можем использовать эту функцию где угодно:
// services/todoService.js
const generateRandomId = require('../utils/randomId');
function createTodo(title) {
if (!title || typeof title !== 'string' || !title.trim()) {
throw new Error('Некорректный заголовок задачи');
}
const newTodo = { id: generateRandomId(), title: title.trim(), completed: false };
todos.push(newTodo);
return newTodo;
}
Ещё примеры utils
- Проверка email (isValidEmail)
- Форматирование даты (formatDate)
- Хэширование паролей (если не используете библиотеку)
- Преобразование строк (например, capitalize)
Важно: В utils/ не должно быть кода, который зависит от структуры приложения или бизнес-логики. Только универсальные штуки!
4. models/: структура данных и схемы
Простой пример без базы данных
Если вы пока не используете базу данных, в models/ могут лежать классы, описывающие ваши объекты.
// models/Todo.js
class Todo {
constructor({ id, title, completed = false }) {
this.id = id;
this.title = title;
this.completed = completed;
}
}
module.exports = Todo;
Теперь сервис может создавать задачи через модель:
// services/todoService.js
const Todo = require('../models/Todo');
const generateRandomId = require('../utils/randomId');
function createTodo(title) {
if (!title || typeof title !== 'string' || !title.trim()) {
throw new Error('Некорректный заголовок задачи');
}
const newTodo = new Todo({ id: generateRandomId(), title: title.trim() });
todos.push(newTodo);
return newTodo;
}
Пример с использованием базы данных
Если вы используете MongoDB и Mongoose, модель будет выглядеть так:
// models/Todo.js
const mongoose = require('mongoose');
const todoSchema = new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
});
module.exports = mongoose.model('Todo', todoSchema);
В сервисе:
// services/todoService.js
const Todo = require('../models/Todo');
async function createTodo(title) {
if (!title || typeof title !== 'string' || !title.trim()) {
throw new Error('Некорректный заголовок задачи');
}
const newTodo = new Todo({ title: title.trim() });
await newTodo.save();
return newTodo;
}
async function getAllTodos() {
return Todo.find();
}
module.exports = {
createTodo,
getAllTodos,
};
Плюсы:
- Вся информация о структуре задачи теперь централизована в модели.
- Если структура меняется — править нужно только модель.
5. Визуализация архитектуры
Вот как выглядит архитектура приложения с разделением на слои:
┌──────────────┐
│ routes/ │ ← Express-маршруты (роуты)
└─────┬────────┘
│
┌─────▼────────┐
│ controllers/ │ ← Контроллеры: принимают запрос, вызывают сервисы
└─────┬────────┘
│
┌─────▼────────┐
│ services/ │ ← Бизнес-логика, работа с моделями, валидация, API
└─────┬────────┘
│
┌─────▼────────┐
│ models/ │ ← Структуры данных, схемы ORM
└──────────────┘
utils/ — вспомогательные функции, используются по всему проекту
Таблица: что где хранить
| Папка | Назначение | Примеры файлов |
|---|---|---|
| routes/ | Определение маршрутов Express | |
| controllers/ | Обработка HTTP-запросов, вызов сервисов | |
| services/ | Бизнес-логика, работа с моделями, сторонние API | |
| models/ | Описание структуры данных, схемы ORM | |
| utils/ | Вспомогательные функции, не зависящие от бизнес-логики | |
6. Практический пример: связываем всё вместе
Допустим, у нас есть задача: реализовать API для задач (todo-list).
Структура проекта:
project/
├── routes/
│ └── todoRoutes.js
├── controllers/
│ └── todoController.js
├── services/
│ └── todoService.js
├── models/
│ └── Todo.js
├── utils/
│ └── randomId.js
└── app.js
app.js
const express = require('express');
const todoRoutes = require('./routes/todoRoutes');
const app = express();
app.use(express.json());
app.use('/api/todos', todoRoutes);
app.listen(3000, () => console.log('Сервер запущен на порту 3000'));
routes/todoRoutes.js
const express = require('express');
const router = express.Router();
const todoController = require('../controllers/todoController');
router.get('/', todoController.getAllTodos);
router.post('/', todoController.createTodo);
module.exports = router;
controllers/todoController.js
const todoService = require('../services/todoService');
exports.getAllTodos = (req, res) => {
res.json(todoService.getAllTodos());
};
exports.createTodo = (req, res) => {
try {
const newTodo = todoService.createTodo(req.body.title);
res.status(201).json(newTodo);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
services/todoService.js
const Todo = require('../models/Todo');
const generateRandomId = require('../utils/randomId');
const todos = []; // временное хранилище
function getAllTodos() {
return todos;
}
function createTodo(title) {
if (!title || typeof title !== 'string' || !title.trim()) {
throw new Error('Некорректный заголовок задачи');
}
const newTodo = new Todo({ id: generateRandomId(), title: title.trim() });
todos.push(newTodo);
return newTodo;
}
module.exports = {
getAllTodos,
createTodo,
};
models/Todo.js
class Todo {
constructor({ id, title, completed = false }) {
this.id = id;
this.title = title;
this.completed = completed;
}
}
module.exports = Todo;
utils/randomId.js
function generateRandomId(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
module.exports = generateRandomId;
7. Как это помогает в реальных проектах
- Масштабируемость: Когда задач становится больше, достаточно добавить новый сервис или модель, не трогая остальные части приложения.
- Тестируемость: Можно тестировать сервисы и utils-функции отдельно, без запуска сервера.
- Переиспользуемость: Вспомогательные функции и сервисы можно использовать в разных частях приложения.
- Читаемость: Новому разработчику проще понять, что где лежит, и не заблудиться в дебрях кода.
8. Типичные ошибки при организации services/, utils/, models/
Ошибка №1: Вся логика в контроллерах.
Очень часто новички пытаются реализовать всю бизнес-логику прямо в контроллерах. В результате контроллеры становятся огромными, их сложно тестировать, и при малейшем изменении бизнес-правил приходится переписывать кучу кода.
Ошибка №2: Смешивание бизнес-логики и вспомогательных функций.
Если вы пишете функции для генерации случайных строк прямо в сервисах, а не выносите их в utils, скоро начнёте их дублировать в разных сервисах. В итоге — куча копипасты и багов при изменении логики.
Ошибка №3: models используются как "мешанина данных".
Иногда в модели начинают добавлять методы, которые относятся к сервисам (например, бизнес-логику). Модель должна описывать структуру данных, а не бизнес-логику! Всё, что связано с правилами работы — в сервисы.
Ошибка №4: utils начинают знать о бизнес-логике.
Если в utils появляются зависимости от сервисов или моделей — значит, вы что-то делаете не так. Utils должны быть максимально универсальными и не зависеть от остального приложения.
Ошибка №5: Нет единого подхода к структуре.
Если в одном месте вы храните бизнес-логику в контроллерах, а в другом — в сервисах, проект быстро превращается в хаос. Старайтесь придерживаться единого стиля!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ