JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /services/, utils/, models/: назначение, примеры использов...

services/, utils/, models/: назначение, примеры использования

Модуль 4: Node.js, Next.js и Angular
6 уровень , 8 лекция
Открыта

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
todoRoutes.js
controllers/ Обработка HTTP-запросов, вызов сервисов
todoController.js
services/ Бизнес-логика, работа с моделями, сторонние API
todoService.js
models/ Описание структуры данных, схемы ORM
Todo.js
utils/ Вспомогательные функции, не зависящие от бизнес-логики
randomId.js, format.js

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: Нет единого подхода к структуре.
Если в одном месте вы храните бизнес-логику в контроллерах, а в другом — в сервисах, проект быстро превращается в хаос. Старайтесь придерживаться единого стиля!

1
Задача
Модуль 4: Node.js, Next.js и Angular, 6 уровень, 8 лекция
Недоступна
Создание utils-функции для форматирования даты
Создание utils-функции для форматирования даты
1
Задача
Модуль 4: Node.js, Next.js и Angular, 6 уровень, 8 лекция
Недоступна
Модель пользователя с классом
Модель пользователя с классом
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ