👾
Node.js
  • 🧑‍💻Full-Stack Web Developer
  • 📚Теорія
    • 1️⃣Основи Node.js
      • Вступ
      • Модулі Node.js
      • Запуск скриптів модулів в Node.js
      • Структура проєкту, експорт-імпорт, index.js як хаб
      • Модулі CommonJS
      • Модулі MJS
      • Модулі ECMAScript
      • Модулі NPM + базові модулі
      • Глобальні змінні
      • Робота з файлами
    • 2️⃣Консольні додатки
      • Створення консольних додатків
    • 3️⃣Фреймворк Express
      • Про Express
      • Nodemon і запуски скриптів
      • Postman
      • Проміжне ПЗ middleware
      • Передача даних на сервер
      • Роутінг
      • CRUD
      • Налаштування лінтера
    • 4️⃣REST API
      • Змінні оточення
      • Логування
      • REST
      • Методи HTTP
      • CORS
      • Формування URL для REST API
      • Контроллери відсутнього роуту і непередбачуваної помилки
      • Валідація даних Joi
      • Рефакторинг додатку за MVC архітектурою
      • Express автогенератор додатку
    • 5️⃣База даних Mongo.DB
      • Основи MongoDB
      • Налаштування Mongo Atlas
      • Встановлення локальної MongoDB і основні команди
    • 6️⃣ODM Mongoose
      • Mongoose
      • Порядок планування бекенд додатку
      • чорнетка
    • 7️⃣Автентифякація WJT
      • чорнетка
      • чорнетка 2
    • 8️⃣Файли
      • чернетка
    • 9️⃣тестування
      • чернетка
    • 🔟Page 14
      • імейли
    • чорнетка докер
    • чорнетка сокети
    • додаткові матеріали
    • 👷Практика
      • 1️⃣Page 4
      • 2️⃣Page 5
      • 3️⃣Page 6
      • 4️⃣Page 7
      • 5️⃣Page 8
      • 6️⃣Page 9
  • Про мене
    • Про мене
Powered by GitBook
On this page
  • Винесення функцій-обробників (Controllers)
  • Роутери (Router)
  • Винесення проміжних функцій (Middlewares)
  • Кастомний обробник помилок
  • Рефакторинг повторюваних паттернів try{}catch(){}
  1. Теорія
  2. REST API

Рефакторинг додатку за MVC архітектурою

PreviousВалідація даних JoiNextExpress автогенератор додатку

Last updated 1 year ago

У backend розробці прийнято не писати весь код в одному файлі, а розділяти його на блоки в залежності від функціонала. Реалізують це у вигляді MVC-архітектури (модель-вид-контроллер). Тобто розділяють бізнес-логіку і презентацію.

Модель - це блоки роботи з базами даних і різні серверні обробки

Вид - блоки, які комунікують із зовнішнім світом. (повернення json, route, html тощо). Тобто те, що віддаємо назовні.

Контролер - забезпечує взаємодію моделі та виду, які не комунікують напряму між собою.

Проведемо рефакторинг за визначеною архітектурою код розглянутий .

Створимо проєкт такої структури:

project
      ├── index.js
      ├── package.json
      ├── usersdb.json
      ├── enviroments
      │             ├── development.env
      │             └── production.env
      ├── models
      ├── controllers
      │             └──userController.js
      ├── middlewares
      │             └──userMiddlewares.js
      ├──routes
      │       └──userRoutes.js
      ├──views
      ├──utils
      │      ├──appError.js
      │      ├──catchAsync.js
      │      └──index.js
      └──servises

index.js - головний файл роботи сервера.

package.json - файл конфігурації проєкту.

usersdb.json - файл, який тимчасово поки виконує роль бази даних.

enviroments - каталог в якому зберігаються файли зі змінними оточення.

models - моделі даних для збереження в базу даних

controllers - каталог, який містить контролери обробки запитів.

middlewares - каталог, який містить проміжне пз.

routes - каталог, який містить роути запитів.

views -

utils - каталог із допоміжними функціями (ваділатори, обробники тощо).

servises - каталог зі стороннім функціоналом (бази даних, функції-конфігуратори, розсилки тощо.

Файли прийнято називати у форматі camelCase (наприклад userController.js), інколи модна побачити синтаксис через крапку (наприклад user.controller.js)

Винесення функцій-обробників (Controllers)

З головного файлу index.js винесемо окремо в userController.js обробники запитів. розглянемо на прикладі створення користувача.

Переносимо в цей файл використовувані бібліотеки

controllers/userController.js
const uuid = require("uuid").v4;
const fs = require("fs").promises;

А також саму функцію контроллера (назвемо її, наприклад createUser):

controllers/userController.js
exports.createUser = async (req, res) => {
  try {
    const { name, year, email } = req.body;
    // тут має бути валідація даних, які прийшли

    //створюємо обʼєкт нового користувача, генеруємо для нього унікальний id
    const newUser = { name, year, email, id: uuid() };

    // зберігаємо користувача в базу даних (usersdb.json)
    const usersDB = await fs.readFile("./usersdb.json");
    const users = JSON.parse(usersDB);
    users.push(newUser);
    await fs.writeFile("./usersdb.json", JSON.stringify(users));

    //надсилаємо відповідь на фронтенд
    res.status(201).json({
      msg: "User created",
      user: newUser,
    });
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

В index.js імпортуємо контролери й підключаємо до роуту

index.js
const userController = require("./controllers/userController");
// some code
app.post("/users", userController.createUser);

Після кожного невеликого рефакторингу не забуваємо перевіряти чи не порушився функціонал.

За аналогією виносимо всі інші контролери також у файл userController.js. Назви їм дамо, наприклад такі: getAllUsers, getOneUser, updateUser, deleteUser.

Нижче розгорни, щоб побачити весь код цього файлу.

Контроллери винесені в окремий файл
controllers/userController.js
const uuid = require("uuid").v4;
const fs = require("fs").promises;

exports.createUser = async (req, res) => {
  try {
    const { name, year, email } = req.body;
    // тут має бути валідація даних, які прийшли

    //створюємо обʼєкт нового користувача, генеруємо для нього унікальний id
    const newUser = { name, year, email, id: uuid() };

    // зберігаємо користувача в базу даних (usersdb.json)
    const usersDB = await fs.readFile("./usersdb.json");
    const users = JSON.parse(usersDB);
    users.push(newUser);
    await fs.writeFile("./usersdb.json", JSON.stringify(users));

    //надсилаємо відповідь на фронтенд
    res.status(201).json({
      msg: "User created",
      user: newUser,
    });
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

exports.getAllUsers = async (req, res) => {
  try {
    const users = JSON.parse(await fs.readFile("./usersdb.json"));
    res.status(200).json({ msg: "Success", users });
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

exports.getOneUser = (req, res) => {
  const { user } = req;

  res.status(200).json({
    msg: "Success",
    user,
  });
};

exports.updateUser = async (req, res) => {
  try {
    const { user } = req;
    const { name, year, email } = req.body;
    const updatedUser = { ...user, name, year, email };

    const users = JSON.parse(await fs.readFile("./usersdb.json"));
    let updatedUsers = [];
    updatedUsers = users.map((item) => {
      if (item.id !== updatedUser.id) return item;
      if (item.id === updatedUser.id) return updatedUser;
    });

    await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));

    res.status(200).json({
      msg: "Success",
      user: updatedUser,
    });
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

exports.deleteUser = async (req, res) => {
  try {
    const { user } = req;

    const users = JSON.parse(await fs.readFile("./usersdb.json"));
    let updatedUsers = [];
    updatedUsers = users.filter((item) => item.id !== user.id);

    await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));
    // як варіант res.sendStatus(204);
    res.status(200).json({
      msg: "Success",
    });
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

А в нашому index.js лишаються гарні лаконічні роути:

index.js
app.post("/users", userController.createUser);
app.get("/users", userController.getAllUsers);
app.get("/users/:userId", userController.getOneUser);
app.patch("/users/:userId", userController.updateUser);
app.delete("/users/:userId", userController.deleteUser);

А також після них пропишемо обробку запиту за неіснуючим роутом і виникнення непередбачуваної помилки сервера.

Розгорни, щоб побачити
index.js
app.all('*', (req, res) => {
  res.status(404).json({
    msg: 'Oops! Resource not found..',
  });
});

app.use((err, req, res, next) => {
  res.status(500).json({
    msg: err.message,
  });
});

Роутери (Router)

Як ми побачили вище, в index.js лишилися обробники роутів користувача. Але сервер може обробляти запити не тільки користувача, але й інші запити й згодом їх перелік стане довгим. Видно, що спільним для них є базовий запит users. У express передбачена можливість винесення однотипних роутів завдяки Router. Підключаємо і створюємо екземпляр роута:

routes/userRoutes.js
const { Router } = require("express");
//some code
const router = Router();

Переносимо імпорт контроллерів з index.js до userRoutes.js

routes/userRoutes.js
const userController = require("../controllers/userController");

Переносимо всі роути з index.js до userRoutes.js і замість app вказуємо router. Крім того прибираємо базовий урл users, бо його ми вкажемо в index.js. І експортуємо наш роутер:

routes/userRoutes.js
router.post("/", userController.createUser);
router.get("/", userController.getAllUsers);
router.get("/:userId", userController.getOneUser);
router.patch("/:userId", userController.updateUser);
router.delete("/:userId", userController.deleteUser);

module.exports = router;

Після цього нам потрібно в index.js заімпортувати роути повʼязані з користувачами і вказати який роут оброблятиме запити за users (синтаксис як у middleware):

index.js
const userRoutes = require("./routes/userRoutes");
//some code
app.use("/users", userRoutes);

ПОКРАЩЕННЯ КОДУ: У файлі роутів можна деструктуризувати наші контроллери і відтак упростити код для читання.

routes/userRoutes.js
const {
  createUser,
  getAllUsers,
  getOneUser,
  updateUser,
  deleteUser,
} = require("../controllers/userController");

router.post("/", createUser);
router.get("/", getAllUsers);
router.get("/:userId", getOneUser);
router.patch("/:userId", updateUser);
router.delete("/:userId", deleteUser);

Альтернативний синтаксис роутів через метод route:

routes/userRoutes.js
router
   .route("/")
   .post(createUser)
   .get(getAllUsers);

router.route("/:userId")
   .get(getOneUser)
   .patch(updateUser)
   .delete(deleteUser);

Винесення проміжних функцій (Middlewares)

Проміжні функції (middleware) також доречно виносити в окремі файли. Наприклад ми стоврювали в index.js кастомний middleware отримання обʼєкту користувача. Давайте його винесемо в окремий файл userMiddlewares.js. Порядок такий само як і при роботі з контроллерами. Підключаємо у файлі необхідні модулі і створюємо функцію та експортуємо її.

middlewares/userMiddlewares.js
const fs = require("fs").promises;

exports.checkUserId = async (req, res, next) => {
  try {
    const { userId } = req.params;
    const users = JSON.parse(await fs.readFile("./usersdb.json"));
    const user = users.find((item) => item.id === userId);
    if (!user) {
      return res.status(404).json({
        msg: "User does not exist",
      });
    }
    req.user = user;
    next();
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
};

Утім підключати цю функцію ми будемо не в index.js, а в userRoutes.js, адже саме на цьому маршруті використовується ця проміжна функція.

routes/userRoutes.js
const { checkUserId } = require("../middlewares/userMiddlewares");

У роуті ми перераховуємо по порядку всі middleware, а останнім вказуємо контроллер обробки запиту. Примітно, що цю проміжну функцію треба використовувати тільки там де в маршруті присутній id:

routes/userRoutes.js
router.post("/", createUser);
router.get("/", getAllUsers);
router.get("/:userId", checkUserId, getOneUser);
router.patch("/:userId", checkUserId, updateUser);
router.delete("/:userId", checkUserId, deleteUser);

Або можна прописати цей middleware перед тим як будуть опрацьовані роути, де він використовується.

routes/userRoutes.js
router
   .route("/")
   .post(createUser)
   .get(getAllUsers);

router.use("/:userId", checkUserId);

router
   .route("/:userId")
   .get(getOneUser)
   .patch(updateUser)
   .delete(deleteUser);

РЕМАРКА: Припустимо у нас є в системі кроистувач, який обробляється не за правилами за запитом маршруту users/bad, у разі, якшо ми його пропигемо вкінці, то він проігнорується. Тому кастомні маршрути, щоб вони опрацювалися, потрібно прописувати перд тими роутами в яких обробка іде за подібною схемою з динамічним параметром у запиті.

Кастомний обробник помилок

Можна написати клас специфічної обробки помилок.

Для цього створимо на базі класу Error наш клас AppError. Він прийматиме статус помилки і повідомлення:

utils/appError.js
class AppError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

module.exports = AppError;

Для полегшення підключення цих модулів створимо в папці utils файл index.js, який буде хабом для модулів в цій папці:

utils/index.js
const AppError = require('./appError');

module.exports = {
  AppError,
};

Для наочності використання цього класу застосуємо його в нашому кастомному middleware. Підключаємо:

middlewares/userMiddlewares.js
const { AppError } = require("../utils");

Пропишемо всередині якусь перевірку. наприклад на довдину id, щоб вона була не короштою за 10 символів, інакше згенеруємо помилку:

middlewares/userMiddlewares.js
 if (userId.length < 10) {
      throw new AppError(400, "Invalid Id...");
    }

В такому разі помилку буде опрацьовувати глобальний хендлер помилки. Для цього потрібно просто замість повернення статусу помилки в секції case(err) {} передати естафету функції next і як параметр передати їй помилку.

middlewares/userMiddlewares.js
catch (err) {
    console.log(err);
    next(err);
    // замість цього: res.sendStatus(500);
  }
Файл кастомного middleware після змін
middlewares/userMiddlewares.js
const fs = require("fs").promises;

const { AppError } = require("../utils");

exports.checkUserId = async (req, res, next) => {
  try {
    const { userId } = req.params;
    if (userId.length < 10) {
      throw new AppError(400, "Invalid Id...");
    }
    const users = JSON.parse(await fs.readFile("./usersdb.json"));
    const user = users.find((item) => item.id === userId);
    if (!user) {
      return res.status(404).json({
        msg: "User does not exist",
      });
    }
    req.user = user;
    next();
  } catch (err) {
    console.log(err);
    next(err);
    // res.sendStatus(500);
  }
};

Тоді в обробнику помилки в index.js потрібно прописати повернення нового коду помилки і повідомлення, або за замовчанням статус 500.

index.js
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({
    msg: err.message,
  });
});

Перевіримо відпрацювання цієї помилки:

Рефакторинг повторюваних паттернів try{}catch(){}

Як бачимо у наших проміжних обробниках і контроллерах постійно повторюється конструкція try{}catch(){}. Підхід Don't repeate yourself каже, що цей код треба покращити.

Напишемо в utils в файлі catchAsync.js функцію-обгортку, яка прийматиме нашу функцію і повертатиме таку саму функцію, а у разі виникнення помилки передаватиме на обробник помилок нашу еррорку.

utils/catchAsync.js
module.exports = (fn) => (req, res, next) => {
  fn(req, res, next).catch((err) => next(err));
};

Експортуємо цю функцію через хаб index.js

utils/index.js
const AppError = require("./appError");
const catchAsync = require("./catchAsync");

module.exports = {
  AppError,
  catchAsync,
};

Тепер випробуємо цей код на прикладі нашого кастомного middleware.

Імпортуємо:

middlewares/userMiddlewares.js
const { AppError, catchAsync } = require("../utils");

Тепер ми огортаємо функцію в catchAsync і прибираємо try{}catch(){}

middlewares/userMiddlewares.js
const { AppError, catchAsync } = require("../utils");

exports.checkUserId = catchAsync(async (req, res, next) => {
  const { userId } = req.params;
  if (userId.length < 10) {
    throw new AppError(400, "Invalid Id...");
  }
  const users = JSON.parse(await fs.readFile("./usersdb.json"));
  const user = users.find((item) => item.id === userId);
  if (!user) {
    return res.status(404).json({
      msg: "User does not exist",
    });
  }
  req.user = user;
  next();
});

так само можна переписати всі асинхронні функції в контролерах.

Розгорни, щоб побачити код після рефакторингу
controllers/userController.js
const uuid = require("uuid").v4;
const fs = require("fs").promises;

const { catchAsync } = require("../utils");

exports.createUser = catchAsync(async (req, res) => {
  const { name, year, email } = req.body;
  // тут має бути валідація даних, які прийшли

  //створюємо обʼєкт нового користувача, генеруємо для нього унікальний id
  const newUser = { name, year, email, id: uuid() };

  // зберігаємо користувача в базу даних (usersdb.json)
  const usersDB = await fs.readFile("./usersdb.json");
  const users = JSON.parse(usersDB);
  users.push(newUser);
  await fs.writeFile("./usersdb.json", JSON.stringify(users));

  //надсилаємо відповідь на фронтенд
  res.status(201).json({
    msg: "User created",
    user: newUser,
  });
});

exports.getAllUsers = catchAsync(async (req, res) => {
  const users = JSON.parse(await fs.readFile("./usersdb.json"));
  res.status(200).json({ msg: "Success", users });
});

exports.getOneUser = (req, res) => {
  const { user } = req;

  res.status(200).json({
    msg: "Success",
    user,
  });
};

exports.updateUser = catchAsync(async (req, res) => {
  const { user } = req;
  const { name, year, email } = req.body;
  const updatedUser = { ...user, name, year, email };

  const users = JSON.parse(await fs.readFile("./usersdb.json"));
  let updatedUsers = [];
  updatedUsers = users.map((item) => {
    if (item.id !== updatedUser.id) return item;
    if (item.id === updatedUser.id) return updatedUser;
  });

  await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));

  res.status(200).json({
    msg: "Success",
    user: updatedUser,
  });
});

exports.deleteUser = catchAsync(async (req, res) => {
  const { user } = req;

  const users = JSON.parse(await fs.readFile("./usersdb.json"));
  let updatedUsers = [];
  updatedUsers = users.filter((item) => item.id !== user.id);

  await fs.writeFile("./usersdb.json", JSON.stringify(updatedUsers));
  // як варіант res.sendStatus(204);
  res.status(200).json({
    msg: "Success",
  });
});

Покликання:

📚
4️⃣
раніше
MVC