JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /controllers/: обработка бизнес-логики, примеры

controllers/: обработка бизнес-логики, примеры

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

1. Что такое контроллеры и зачем они нужны

Давайте представим Express-приложение как ресторан.
Маршруты (routes/) — это официанты, которые принимают заказы (HTTP-запросы) и решают, на какую кухню (контроллер) отправить заказ.
Контроллеры (controllers/) — это повара, которые знают, как готовить блюда (обрабатывать бизнес-логику запроса).
Сервисы (services/) — это кладовая и рецепты: тут хранится логика работы с данными, база знаний, интеграция с внешними сервисами.

Если официантов (routes) заставить готовить еду (писать бизнес-логику прямо в маршрутах), ресторан быстро превратится в хаос. Поэтому мы выносим обработку "готовки" в отдельные контроллеры.

Контроллер — это функция или набор функций, которые принимают запрос, делают нужные вычисления, работают с сервисами/моделями и отправляют ответ клиенту.

Зачем выносить логику в контроллеры?

  • Чистота и читаемость кода: маршруты становятся короткими и понятными.
  • Повторное использование: одну и ту же бизнес-логику можно использовать из разных маршрутов.
  • Тестируемость: контроллеры проще тестировать отдельно.
  • Лёгкость поддержки: если поменялись бизнес-правила, меняем только контроллер.

2. Пример структуры проекта с контроллерами

Рассмотрим структуру простого ToDo-приложения:


project-root/
├── app.js
├── routes/
│   └── tasks.js
├── controllers/
│   └── tasksController.js
├── services/
│   └── tasksService.js
└── data/
    └── tasks.json
  • routes/tasks.js — определяет маршруты (GET /tasks, POST /tasks и т.д.) и вызывает соответствующие контроллеры.
  • controllers/tasksController.js — содержит функции, которые реализуют бизнес-логику: например, получение списка задач, добавление новой задачи и т.д.
  • services/tasksService.js — отдельный слой для работы с данными (например, с файлами или базой данных). Это не обязательно, но для крупных проектов — очень удобно.

3. Как выглядит контроллер на практике

Пример: контроллер для задач (tasksController.js)

Начнём с простого варианта, когда данные хранятся просто в памяти (или в файле JSON).


// controllers/tasksController.js

const tasks = require('../data/tasks.json'); // Для простоты — массив задач

// Получить все задачи
function getAllTasks(req, res) {
  res.json(tasks);
}

// Получить задачу по id
function getTaskById(req, res) {
  const id = Number(req.params.id);
  const task = tasks.find(t => t.id === id);
  if (!task) {
    return res.status(404).json({ error: 'Задача не найдена' });
  }
  res.json(task);
}

// Добавить новую задачу
function createTask(req, res) {
  const { title } = req.body;
  if (!title) {
    return res.status(400).json({ error: 'Поле title обязательно' });
  }
  const newTask = {
    id: tasks.length ? tasks[tasks.length - 1].id + 1 : 1,
    title,
    completed: false
  };
  tasks.push(newTask);
  res.status(201).json(newTask);
}

// Экспортируем функции контроллера
module.exports = {
  getAllTasks,
  getTaskById,
  createTask
};

Комментарии:
— Каждый контроллер — обычная функция, принимающая (req, res).
— Контроллер отвечает только за "кухню": что делать с запросом, как обработать данные, какой ответ отправить.
— Все детали работы с данными можно выносить в сервисы (services/), чтобы контроллер был ещё чище.

Как подключить контроллеры к маршрутам


// routes/tasks.js

const express = require('express');
const router = express.Router();
const tasksController = require('../controllers/tasksController');

// GET /tasks — получить все задачи
router.get('/', tasksController.getAllTasks);

// GET /tasks/:id — получить задачу по id
router.get('/:id', tasksController.getTaskById);

// POST /tasks — создать новую задачу
router.post('/', tasksController.createTask);

module.exports = router;

В результате, файл маршрутов — это просто список правил: какой маршрут вызывает какую функцию.

4. Разделение ответственности: контроллеры vs сервисы

В маленьких проектах контроллер может сам работать с данными. Но если проект растёт, лучше добавить слой сервисов:

  • Контроллер: получает запрос, валидирует данные, вызывает нужный метод сервиса, отправляет ответ.
  • Сервис: вся логика работы с данными (поиск, фильтрация, сохранение, интеграция с внешними API).

Пример контроллера с сервисом:


// controllers/tasksController.js

const tasksService = require('../services/tasksService');

async function getAllTasks(req, res) {
  const tasks = await tasksService.getAll();
  res.json(tasks);
}

async function getTaskById(req, res) {
  const id = Number(req.params.id);
  const task = await tasksService.getById(id);
  if (!task) {
    return res.status(404).json({ error: 'Задача не найдена' });
  }
  res.json(task);
}

async function createTask(req, res) {
  const { title } = req.body;
  if (!title) {
    return res.status(400).json({ error: 'Поле title обязательно' });
  }
  const newTask = await tasksService.create({ title });
  res.status(201).json(newTask);
}

module.exports = {
  getAllTasks,
  getTaskById,
  createTask
};

tasksService.js может выглядеть так:


// services/tasksService.js

const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, '../data/tasks.json');

// Чтение всех задач
async function getAll() {
  const raw = await fs.readFile(filePath, 'utf-8');
  return JSON.parse(raw);
}

// Поиск задачи по id
async function getById(id) {
  const tasks = await getAll();
  return tasks.find(t => t.id === id);
}

// Создание новой задачи
async function create({ title }) {
  const tasks = await getAll();
  const newTask = {
    id: tasks.length ? tasks[tasks.length - 1].id + 1 : 1,
    title,
    completed: false
  };
  tasks.push(newTask);
  await fs.writeFile(filePath, JSON.stringify(tasks, null, 2));
  return newTask;
}

module.exports = {
  getAll,
  getById,
  create
};

В результате:
— Контроллеры не зависят от того, где и как хранятся данные.
— Если вы решите перейти с файлов на базу данных, меняете только сервис, контроллеры не трогаете.

5. Пример: обработка ошибок и валидация в контроллере

Контроллеры — отличное место для базовой валидации входных данных и обработки ошибок.


async function createTask(req, res) {
  try {
    const { title } = req.body;
    if (!title || typeof title !== 'string') {
      // Проверяем, что поле title есть и это строка
      return res.status(400).json({ error: 'Поле title обязательно и должно быть строкой' });
    }
    const newTask = await tasksService.create({ title });
    res.status(201).json(newTask);
  } catch (err) {
    // Неожиданная ошибка — отправляем 500
    res.status(500).json({ error: 'Ошибка сервера', details: err.message });
  }
}

Зачем так делать?
— Если что-то пошло не так, клиент получит понятное сообщение.
— Валидация данных на уровне контроллера — это "первая линия обороны" от мусорных запросов.

6. Полезные нюансы

Как связать всё вместе в основном приложении

В файле app.js (или index.js) подключаем маршруты:


const express = require('express');
const app = express();

app.use(express.json()); // Для работы с JSON-телом запроса

const tasksRouter = require('./routes/tasks');
app.use('/tasks', tasksRouter);

app.listen(3000, () => {
  console.log('Сервер запущен на http://localhost:3000');
});

Советы по организации контроллеров

  • Один контроллер — одна сущность (например, tasksController для задач, usersController для пользователей).
  • Если контроллер становится слишком большим — разбивайте на отдельные файлы или группы функций.
  • Не пишите в контроллере SQL-запросы, работу с файлами или сторонними API — для этого есть сервисы.
  • Если логика совсем простая (например, вернуть "Hello, world!") — можно обойтись без контроллера, но для реальных приложений всегда выделяйте этот слой.

7. Практика: расширяем приложение

Добавим ещё один контроллер — удаление задачи:


// controllers/tasksController.js

async function deleteTask(req, res) {
  const id = Number(req.params.id);
  const deleted = await tasksService.deleteById(id);
  if (!deleted) {
    return res.status(404).json({ error: 'Задача не найдена' });
  }
  res.json({ message: 'Задача удалена' });
}

module.exports = {
  // ...другие контроллеры,
  deleteTask
};

В сервисе:


// services/tasksService.js

async function deleteById(id) {
  const tasks = await getAll();
  const index = tasks.findIndex(t => t.id === id);
  if (index === -1) return false;
  tasks.splice(index, 1);
  await fs.writeFile(filePath, JSON.stringify(tasks, null, 2));
  return true;
}

module.exports = {
  // ...другие методы,
  deleteById
};

В маршрутах:


// routes/tasks.js

router.delete('/:id', tasksController.deleteTask);

8. Типичные ошибки при работе с контроллерами

Ошибка №1: Вся логика пишется прямо в маршрутах.
В результате, файл маршрутов превращается в монстра на 300 строк, где смешаны проверки, работа с данными, отправка ответов. Такой код тяжело читать, тестировать и поддерживать.

Ошибка №2: Контроллер занимается всем подряд.
Если контроллер сам валидирует, сам лезет в базу, сам отправляет почту и ещё и считает скидку — это сигнал, что пора вынести части логики в сервисы или утилиты.

Ошибка №3: Нет обработки ошибок.
Если контроллер не ловит исключения, приложение может "упасть" при любой ошибке в сервисе, а клиент получит неинформативный ответ или вообще ничего.

Ошибка №4: Не проверяются входные данные.
Если не валидировать данные в контроллере, можно получить неожиданные ошибки в сервисе или даже "дырки" в безопасности.

Ошибка №5: Нарушение принципа единой ответственности.
Контроллер должен только "рулить" бизнес-логикой обработки запроса, а не заниматься всем подряд.

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