Три года назад я начал разработку Express.js API для одной компании. Тогда я подумал: какой должна быть идеальная архитектура контроллеров для правильной организации разрастающейся базы кода?
Впечатлившись Sails & Rails и проведя собственные исследования, я смог разработать собственную систему. Мне не хотелось перегружать проект полноценным фреймворком по типу Sails, и я больше склонялся к ситуационному подбору более легких зависимостей.
В результате я смог создать организационную систему для контроллеров приложения, которую дополнил самописным загрузчиком. Опыт, полученный при реализации данной структуры в других проектах, позволил мне доработать эту систему и получить эффективные результаты.
Насколько мне известно, моей системой уже воспользовалось несколько крупных компаний. Она упрощала адаптацию новых разработчиков, обеспечивая большую читабельность кодовой базы.
Структура
Если не учитывать дальнейший рост приложения, то вы быстро столкнетесь с неорганизованной базой кода. Я создал организационный метод с широкой совместимостью. Это означает, что вы никогда не окажетесь в ситуации, когда данный метод не сможет решить какую-то конкретную проблему в сценарии использования.
Настройте файловое дерево
- Сгруппируйте маршруты в контроллерах.
- Создайте папки для каждого контроллера.
- В каждом контроллере создайте файл маршрутизации, который описывает путь каждого маршрута, вызываемый метод, связанную с ним функцию промежуточной обработки и уровень ограничений.
- Для каждого действия контроллера добавьте файл, содержащий выполняемый метод и функции промежуточной обработки.
- Создайте spec файл для тестирования.
Давайте посмотрим, что получилось.
Не бойтесь создавать много файлов. Это не замедлит разработку. Зато сделает кодовую базу аккуратнее и понятнее.
Загрузите маршруты
Для обеспечения работоспособности такой структуры необходимо воспользоваться простым самописным загрузчиком: Lumie. Он пробежится по контроллерам, прочитает файлы определений и загрузит ваши маршруты.
Это небольшой пакет. Его исходный код можно просмотреть на GitHub.
Файлы маршрутизации
По своему определению такие файлы должны быть легко читаемыми. Вы должны просмотреть .routing файлы и сразу понять, какие именно методы следует обновить. В этом примере были созданы 3 маршрута:
- [ PUT ] /user
- [ GET ] /user
- [ GET ] /user/reset-password
const get = require("./get.action");
const update = require("./update.action");
const resetPassword = require("./resetPassword.action");
module.exports = {
'/': {
put: {
action: update.action,
middlewares: update.middlewares,
level: 'member'
},
get: {
action: get,
level: 'public'
}
},
'/reset-password': {
get: {
action: resetPassword,
middlewares: resetPassword.middlewares,
level: 'public'
}
}
};
controllers/user/user.routing.js
У вас, наверное, возник вопрос: а почему в маршрутах добавляется префикс user, хотя он не фигурирует в определении маршрута? Lumie добавляет префиксы маршрутов по названию папки, в которой находятся файлы маршрутизации.
В нашем примере это — controllers/user/user.routing.js
. Если, например, папка user
находится в подпапке admin
, то префикс маршрутов будет admin/user
.
Обратите внимание, что в определение маршрута можно передавать необязательное поле path
. В таком случае оно заменит стандартное значение.
Действия и функции промежуточной обработки
Как вы уже заметили, каждая конфигурация маршрута содержит метод Action
, представляющий собой не более, чем логику выполнения при вызове API маршрута. Я советую объединять в один файл метод вызова и его функцию промежуточной обработки.
const { body } = require('express-validator/check');
/**
* Middlewares
*/
module.exports.middlewares = [
body('email', 'Invalid email').isEmail(),
body('password', 'username invalid').exists()
];
/**
* Action
*/
module.exports.action = (req, res, next) => {
res.status(200).json({ msg: 'User updated!' });
};
Ограничения
Для каждой конфигурации маршрута вам нужно задать связанный с ним уровень ограничений. Это значение будет передаваться в функцию ограничений. Ее вам тоже потребуется создать. Вот, как инициировать Lumie через вашу ограничительную функцию.
Это должна быть функция, которая возвращает классическую функцию промежуточной Express обработки.
const levelFcts = {
public: (req, res, next) => next(),
member: (req, res, next) => (req.user ? next() : res.sendStatus(401))
};
module.exports = level => (req, res, next) => levelFcts[level](req, res, next);
Пример менеджера основных ограничений
Заключение
Я уже давно пользуюсь этим методом. Мне нравится, что при разработке всегда можно прибегнуть к такому удобному фреймворку. Он позволяет поддерживать аккуратную базу кода и не бросаться в крайности, прописывая слишком много логики в одном файле или перенося определения маршрута не туда.
Перевод статьи Alexandre Levacher: How To Organize Express Controllers For Large Codebases