<div><img src="https://top-fwz1.mail.ru/counter?id=3548135;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div>
Публичное облако на базе VMware с управлением через vCloud Director
Вход / Регистрация

Создание интернет-магазина на Node.js с нуля. Часть 2: Бэкенд

Вячеслав Печенов
Вячеслав Печенов
Технический писатель
02 июня 2025 г.
34
38 минут чтения
Средний рейтинг статьи: 5

В этой статье мы продолжаем рассказывать о процессе создания интернет магазина. В первой части вы узнали, как создать дизайн сайта в Figma, а сегодня мы переходим к этапу создания RestAPI для нашего интернет-магазина. Для его создания мы используем Node.js, MongoDB и Mongoose для взаимодействия с ней. Перед началом написания Backend’а давайте изучим теорию про RestAPI и Mongoose.

Ознакомиться с полным кодом из данной статьи можно на GitHub

that cloud

RestAPI

REST API (Representational State Transfer Application Programming Interface) — это архитектура построения веб-сервисов, основанная на принципах REST (представления состояния передачи), позволяющая клиентам взаимодействовать с сервером, запрашивая ресурсы (например, данные или операции над ними) посредством стандартных HTTP-запросов.

Ключевые принципы REST API

  • Использование HTTP-методов: Каждый метод имеет свое предназначение и соответствует конкретной операции CRUD (Create, Read, Update, Delete):

    • GET — получение ресурса (чтение)

    • POST — создание нового ресурса

    • PUT/PATCH — обновление существующего ресурса

    • DELETE — удаление ресурса

  • Адресация ресурсов: Ресурсы обозначаются уникальными URI (Uniform Resource Identifiers), которые однозначно определяют местоположение ресурса в пространстве имен сервера.

  • Без состояний (Stateless): Сервер не хранит состояние клиента между запросами. Вся необходимая информация передается клиентом вместе с каждым запросом.

  • Кэшируемость: Ответы от сервера могут кэшироваться, что улучшает производительность и снижает нагрузку на сервер.

  • Используемые форматы обмена данными: Чаще всего применяются JSON или XML, хотя возможны и другие форматы (например, YAML).

  • Обработка ошибок: Стандартизированные коды HTTP позволяют передавать клиенту статус успешности/неуспешности операций и причины возникновения ошибок.

Преимущества REST API

  • Простота разработки и поддержки благодаря использованию общепринятых стандартов (HTTP);
  • Гибкость и возможность интеграции с различными платформами и языками программирования;
  • Масштабируемость и отказоустойчивость за счет архитектуры stateless;
  • Возможность легкой интеграции с фронтендом (JavaScript, мобильные приложения и др.).

Таким образом, REST API — стандартизованный способ организации удаленного взаимодействия клиент-серверных приложений, который предоставляет четкую структуру и правила взаимодействия, удобные для разработчиков и пользователей сервисов.

Mongoose

Mongoose — это библиотека для Node.js, представляющая собой объектно-документный маппер (ODM) для базы данных MongoDB. Она служит мостом между вашей моделью данных и хранилищем, облегчает взаимодействие с базой данных и улучшает качество вашего кода.

Зачем нужен Mongoose

  • Моделирование данных: Вы можете определить схему ваших документов, включая типы данных, требования и ограничения. Например, вы определяете поля и проверяете их правильность перед записью в базу.

  • Валидаторы и проверка данных: Mongoose позволяет задать правила проверки данных, такие как обязательность полей, допустимые диапазоны значений, регулярные выражения и многое другое.

  • Средства сериализации: Когда вы извлекаете документ из базы данных, Mongoose преобразует его в удобный объект JavaScript, который можно использовать в вашем приложении.

  • Автоматическое управление версиями и история изменений: Через специальные плагины и расширения Mongoose поддерживает ведение истории изменений записей.

  • Поддержка сложных структур данных: В отличие от баз данных SQL, MongoDB позволяет сохранять вложенные структуры данных прямо внутри документов. Mongoose помогает удобно манипулировать такими объектами.

  • Ограничения целостности данных: Поддерживает отношения «один ко многим» и «многие ко многим», обеспечивая согласованность данных и связывая документы друг с другом.

  • Хуки и middleware: Можно добавить промежуточные обработчики (middleware), выполняющиеся до или после определенных действий (создание, чтение, обновление, удаление), что облегчает реализацию бизнес-логики.

  • Синхронизация и асинхронность: Mongoose работает синхронно с использованием обратных вызовов или асинхронно с промисами или async/await.

При создании модели в Mongoose можно указывать ряд различных параметров и конфигураций, которые влияют на поведение и свойства самой модели и ее экземпляров. Эти настройки помогают точно настроить взаимодействие с базой данных, обеспечивают дополнительную безопасность и контроль над качеством данных.

Вот основные параметры и опции, доступные при создании модели.

Типы данных

Типы данных являются основой любой схемы Mongoose. Они указывают, какого типа должно быть значение каждого поля в документе. Примеры базовых типов:

  • String: Используется для хранения строковых значений. Например, имена пользователей, адреса электронной почты и т.п.
  • Number: Для численных значений — целых чисел (Integer) и вещественных (Float).
  • Boolean: Логический тип данных, принимающий значения true или false.
  • Date: Хранит временные метки (объекты типа JavaScript Date). Часто используется для полей вроде даты регистрации, последнего обновления записи и т.п.
  • Buffer: Применяется для бинарных данных, например изображений или файлов.
  • ObjectId: Уникальные идентификаторы объектов, автоматически генерируемые MongoDB. Обычно используются для связи между коллекциями (foreign keys).
  • Mixed: Позволяет хранить данные любого типа. Полезно, если структура данных заранее неизвестна или гибкая.
  • Array: Массив элементов одного или разных типов.
  • Map: Использует объекты Map для динамических ключей и значений.

Требования к данным (Validation)

Можно задать дополнительные условия, которым должны соответствовать поля при сохранении в базу данных. Наиболее распространенные варианты:

  • required: обязательное наличие значения.
  • unique: уникальное значение среди всех документов данной коллекции.
  • default: значение по умолчанию.
  • min/max: минимальное/максимальное число символов, длина массива и т.д.
  • enum: перечисляемое значение, ограничивающее возможные варианты.
  • match: регулярное выражение для соответствия определенному шаблону.
  • validate: произвольная функция для дополнительной проверки данных.

Опции виртуальных полей (Virtuals)

Виртуальные поля позволяют вычислять значения на лету, основываясь на реальных полях документа. Они полезны для вывода расширенной информации, доступной только при чтении документа.

Пример виртуального поля:

const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
}, { toJSON: { virtuals: true } }); // Включаем поддержку виртуалов при сериализации

userSchema.virtual('fullName').get(function () {
  return `${this.firstName} ${this.lastName}`;
});

Теперь каждый раз, когда документ извлекается из базы данных, доступ к полю fullName вернет полное имя пользователя.

Средства контроля и оптимизации производительности

Эти параметры задают внутренние характеристики поведения модели:

  • collection: имя коллекции, куда будут помещаться документы. Если не задано, оно формируется автоматически на основе имени модели.
  • strict: режим строгого соблюдения схемы (по умолчанию включено). Запрещает сохранение полей, отсутствующих в схеме.
  • autoIndex: автоматическая индексация полей, указанных в схеме.
  • timestamps: автоматически добавляет поля createdAt и updatedAt, обновляемые при изменении документа.

Хуки (Middleware)

Вы можете назначать функции, которые будут выполняться до или после определенных операций (save, update, delete и т.д.). Это полезно для предварительной подготовки данных, отправки уведомлений, ведения логов и многого другого.

Связанные модели (References)

Mongoose позволяет устанавливать связи между моделями (документами), используя специальный тип данных ObjectId. Эта связь аналогична внешним ключам в реляционных СУБД.

Пример связанной модели:

const CommentSchema = new mongoose.Schema({
  text: String,
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } 
});

Здесь author ссылается на коллекцию Users

Используя механизм population, можно получать связанные документы:

Comment.findOne().populate('author').exec((err, comment) => {
  if (err) throw err;
  console.log(comment.author.username);
});

Создание модели в Mongoose включает настройку множества аспектов, начиная от обязательных правил и ограничений, заканчивая поддержкой отношений между документами и интеграцией вспомогательных функций. Всё это позволяет строить высокопроизводительные и надежные решения, ориентированные на MongoDB.

Подготовка рабочего пространства

Подготовим наше пространство для работы с Node.js и cron-js.

Установка Node.js и npm

Чтобы запустить локальную разработку, нужно установить актуальную версию Node.js (рекомендуем v22.14.0 LTS). Вместе с Node.js установится npm — стандартный менеджер пакетов JavaScript.

Windows

Перейдите на официальный сайт nodejs.org и скачайте установщик.

Запустите его и следуйте инструкциям.

Linux / MacOS

Выполните в терминале команды:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
\. "$HOME/.nvm/nvm.sh"
nvm install 22

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

node -v && npm -v

Убедитесь, что версии Node.js и npm корректно отображаются.

Настройка директории

  1. Создайте новый каталог для проекта и сразу же перейдите в него:

mkdir OnlineStore-RestAPI && cd OnlineStore-RestAPI
  1. Инициализируйте проект:

npm init -y
  1. Установите необходимые зависимости:

npm install express mongoose nodemailer dotenv body-parser axios
npm install --save-dev nodemon

Рассмотрим, для чего нам необходимы зависимости:

  • Express: веб-фреймворк для серверной части приложений Node.js.
  • Mongoose: библиотека для взаимодействия с базой данных MongoDB, упрощающая работу с моделями данных.
  • Nodemailer: модуль для отправки электронных писем с сервера.
  • Dotenv: утилита для загрузки переменных окружения из файла .env.
  • Body-Parser: middleware для парсинга тела HTTP-запросов.
  • Axios: библиотека для отправки HTTP запросов.
  • Nodemon: инструмент разработчика, предназначенный для автоматического перезапуска узла (Node.js-приложения), когда обнаруживаются изменения в файлах проекта. Устанавливаем его отдельно, так как он нужен нам только в процессе разработки.

Настройка package.json

Изменим наш package.json для того, чтобы работал nodemon и зависимости были модулями. Обновленный файл package.json:

{
  "name": "OnlineStore-RestAPI",
  "version": "1.0.0",
  "description": "RestAPI for OnlineStore",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "nodemon index.js"
  },
  "author": "Timeweb.cloud",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.9.0",
    "body-parser": "^1.20.2",
    "dotenv": "^16.3.2",
    "express": "^4.18.2",
    "mongoose": "^8.1.0",
    "nodemailer": "^7.0.3"
  },
  "devDependencies": {
    "nodemon": "^3.0.3"
  }
}

База данных

Для удобства работы в проекте сделаем следующую организацию файлов:

Image1

  • Папка controller для всех наших контроллеров
  • Папка function для дополнительных функций, которые мы будем использовать
  • Папка model для моделей MongoDB
  • Папка node_modules — стандартная папка при установке зависимостей
  • Папка routes для назначения адресов
  • Файл .env с настройками проекта
  • Файл index.js — главный файл проекта
  • Файлы package-lock.json и package.json с данными о проекте

Создание модели пользователя

Для создания конструкции документов необходимо создать файл модели. Начнем с модели пользователя. Переходим в папку model и создаем файл userModel.js.

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
    firstName : {
        type : String, 
        required : true,
    }, lastName : { 
        type : String, 
        required : true,
    }, email: {
        type: String,
        unique: true,

    }, phone : {
        type: Number,
        unique: true,

    }, address : {
        type: String,
        
    }, region: {
        type: String,

    }, city : {
        type: String,

    }, postCode : {
        type: String,
    }
})

export default mongoose.model("users",userSchema)

Если представить в виде таблицы, то наша схема выглядит следующим образом:

firstName

Текстовое

Обязательное

lastName

Текстовое

Обязательное

email

Текстовое

Уникальное

phone

Числовое

Уникальное

address

Текстовое

region

Текстовое

city

Текстовое

postCode

Текстовое

Создание модели товаров

Создайте файл productModel.js и добавьте следующий код:

import mongoose from "mongoose";

const productSchema = new mongoose.Schema({
    title : {
        type : String, 
        required : true,
        unique: true,
    }, subtitle : { 
        type : String, 
        required : true,
        unique: true,
    }, article: {
        type: String,
        required : true,
        unique: true,

    }, brand : {
        type: String,
        required : true,

    }, oldPrice: {
        type: Number,

    }, price : {
        type: Number,
        required: true,
    }, path: {
        type: Array,
        required: true,
    }, specifications: {
        type: Object,
        of: String,
        required : true,
    }, images: {
        type: Array,
        required : true,
    }
})

export default mongoose.model("product",productSchema)

Создание модели заказов

Создайте файл orderModel.js и добавьте следующий код:

import mongoose from "mongoose";

var createdAt = function(){
    const now = new Date();
    const formattedDate = now.toISOString();
    return formattedDate;
};


const orderSchema = new mongoose.Schema({
    firstName : {
        type : String, 
        required : true,
    }, lastName : { 
        type : String, 
        required : true,
    }, email: {
        type: String,
        required : true,
    }, phone : {
        type: Number,
        required : true,
    }, company: {
        type: String,
    }, address : {
        type: String,
        required : true,
        
    }, region: {
        type: String,
        required : true,

    }, city : {
        type: String,
        required : true,

    }, postCode : {
        type: String,
        required : true,
    }, products: {
        type: Object,
        required : true,
    }, status : {
        type: Object,
        required : true,
    }, price : {
        type: Number,
        required : true,
    }, createdAt: {
        type: String,
        default: createdAt
    },
})

export default mongoose.model("order",orderSchema)

В данном файле также присутствует функция createdAt. Мы используем ее, чтобы получить дату в определенном виде для дальнейшей работы.

Создание контроллеров

Контроллер — это компонент архитектуры MVC (Model–View–Controller), ответственный за обработку входящих HTTP-запросов, выполнение необходимой бизнес-логики и отправку результата обратно клиенту. Контроллеры служат посредниками между входящими запросами и логикой приложения, эффективно отделяя уровень представления (view) от уровня данных (model).

Создание контроллера пользователя

В начале файла каждого контроллера у нас находится импорт модели, с которой мы работаем. Для пользователя это модель userModel.js, пишем в файл userController.js:

import User from "../model/userModel.js"

Нам необходимо создать четыре операции: Create, Read, Update, Delete (CRUD).

Создание нового пользователя

Для создания пользователя нам нужно реализовать следующую логику:

  1. Получаем JSON.
  2. Проверяем, что такого пользователя не существует.
  3. Если пользователь уникален, добавляем запись.

Для проверки уникальности мы будем использовать почту, так как остальные данные могут быть не уникальными.

Перейдите в папку controller, создайте файл userController.js и напишите следующий код: 

export const create = async(req, res)=>{
    try {
        const userData = new User( req.body);
        const {email} = userData;
        const userExist = await User.findOne({email})
        if (userExist){
            return res.status(400).json({message : "User already exists."})
        }
        const savedUser = await userData.save();
        res.status(200).json(savedUser)
    } catch (error) {
        res.status(500).json({error : "Internal Server Error. "})
    }
}

Для определения уникальности пользователя мы получаем из JSON почту и, при помощи метода findOne с переданным значением почты, получаем либо пустое значение, либо запись пользователя. 

Если пользователь уже существует, контроллер вернет статус 400 и напишет «User already exists» (пользователь уже существует). В случае появления каких либо ошибок контроллер вернёт статус 500 и напишет «Internal Server Error» (внутренняя ошибка сервера).

Получение всех пользователей

export const fetch = async (req, res)=>{
    try {
        const users = await User.find();
        if(users.length === 0 ){
            return res.status(404).json({message : "User not Found."})
        }
        res.status(200).json(users);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

Здесь выполняется проверка на наличие хоть каких-то записей пользователей. Если они отсутствуют, то контроллер отправит статус 404 и сообщение «User not Found» (пользователь не найден). Как и ранее, добавлен обработчик ошибок сервера.

Обновление данных пользователя

export const update = async (req, res)=>{
    try {
        const id = req.params.id;
        const userExist = await User.findOne({_id:id})
        if (!userExist){
            return res.status(404).json({message : "User not found."})
        }
        const updateUser = await User.findByIdAndUpdate(id, req.body, {new : true});
        res.status(201).json(updateUser);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

Здесь мы получаем из запроса id пользователя и ищем его в базе. Если он отсутствует, то контроллер отправит статус 404 и сообщение «User not Found» (пользователь не найден). Если пользователь найден, то при помощи метода findByIdAndUpdate происходит обновление данных в записи. Как и ранее, добавлен обработчик ошибок сервера.

Удаление пользователя

Перед созданием функции для удаления стоит отметить что есть два варианта удаления записей из базы данных: soft-delete (мягкое удаление) и hard-delete (жесткое удаление). 

hard-delete — это обычное удаление записи. А soft-delete — это техника удаления данных, при которой запись физически не уничтожается из базы данных, а помечается специальным флагом, означающим, что она больше не должна отображаться пользователям или считаться активной. Такой подход широко применяется там, где важно сохранить историю изменения данных или предотвратить потерю важной информации. 

Обычно при мягком удалении добавляется специальное поле (как правило, называется deletedAt, isDeleted, archived и пр.), которое устанавливается в boolean-значение (или в дату удаления), когда выполняется операция удаления. Затем этот флаг учитывается при выборке данных, фильтруя удаленные элементы из результатов.

В данном примере у нас нет необходимости хранить старых пользователей, поэтому мы используем hard-delete.

export const deleteUser = async (req, res)=>{
    try {
        const id = req.params.id;
        const userExist = await User.findOne({_id:id})
        if(!userExist){
            return res.status(404).json({message : " User Not Found. "})
        }
        await User.findByIdAndDelete(id);
        res.status(201).json({message : " User deleted Successfully."})
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

Как и ранее, у нас добавлена проверка на наличие пользователя, обработчик внутренних ошибок и вывод после удаления пользователя.

Создание контроллера для продуктов

По аналогии с контроллером пользователя создадим контроллер для заказов. Создайте файл productController.js и добавьте следующий код:

import Product from "../model/productModel.js"

export const create = async(req, res)=>{
    try {
        const productData = new Product( req.body);
        const {title} = productData;
        const productExist = await Product.findOne({title})
        if (productExist){
            return res.status(400).json({message : "Product already exist."})
        }
        const savedProduct = await productData.save();
        res.status(200).json(savedProduct)
    } catch (error) {   
        res.status(500).json({error : "Internal Server Error. "})
    }
}

export const fetch = async (req, res)=>{
    try {
        const products = await Product.find();
        if(products.length === 0 ){
            return res.status(404).json({message : "Product not Found."})
        }
        res.status(200).json(products);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

export const fetch2 = async (req, res)=>{
    try {
        const id = req.params.id;
        const products = await Product.findOne({_id:id})
        if(products.length === 0 ){
            return res.status(404).json({message : "Product not Found."})
        }
        res.status(200).json(products);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

export const update = async (req, res)=>{
    try {
        const id = req.params.id;
        const productExist = await Product.findOne({_id:id})
        if (!productExist){
            return res.status(404).json({message : "Product not found."})
        }
        const updateProduct = await Product.findByIdAndUpdate(id, req.body, {new : true});
        res.status(201).json(updateProduct);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}
export const deleteProduct = async (req, res)=>{
    try {
        const id = req.params.id;
        const productExist = await Product.findOne({_id:id})
        if(!productExist){
            return res.status(404).json({message : " Product Not Found. "})
        }
        await Product.findByIdAndDelete(id);
        res.status(201).json({message : " Product deleted Successfully."})
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

Создание контроллера заказов

Создайте файл orderController.js и добавьте следующий код:

import Order from "../model/orderModel.js"

export const create = async(req, res)=>{
    try {
        const orderData = new Order( req.body);
        const {createdAt} = orderData;
        const orderExist = await Order.findOne({createdAt})
        if (orderExist){
            return res.status(400).json({message : "Order already exist."})
        }
        const savedOrder = await orderData.save();
        res.status(200).json(savedOrder)
        
    } catch (error) {   
        console.log(error);
        
        res.status(500).json({error : "Internal Server Error. "})
    }
}

export const fetch = async (req, res)=>{
    try {
        const orders = await Order.find();
        if(orders.length === 0 ){
            return res.status(404).json({message : "Order not Found."})
        }
        res.status(200).json(orders);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}
export const fetch2 = async (req, res)=>{
    try {
        const orders = await Order.find();
        if(orders.length === 0 ){
            return res.status(404).json({message : "Order not Found."})
        }
        const filteredData = orders.filter((order) => {
            return !Object.keys(order.status).some(key => key === "Заказ вручен! Спасибо за покупку");
          });
        res.status(200).json(filteredData);
    } catch (error) {
        console.log(error);
        
        res.status(500).json({error : " Internal Server Error. "})
    }
}
export const fetch3 = async (req, res)=>{
    try {
        const orders = await Order.find();
        if(orders.length === 0 ){
            return res.status(404).json({message : "Order not Found."})
        }
        const filteredData = orders.filter((order) => {
            return Object.keys(order.status).some(key => key === "Заказ вручен! Спасибо за покупку");
          });
        res.status(200).json(filteredData);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

export const update = async (req, res)=>{
    try {
        const id = req.params.id;
        const orderExist = await Order.findOne({_id:id})
        if (!orderExist){
            return res.status(404).json({message : "Order not found."})
        }
        const updateOrder = await Order.findByIdAndUpdate(id, req.body, {new : true});
        res.status(201).json(updateOrder);
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

export const deleteProduct = async (req, res)=>{
    try {
        const id = req.params.id;
        const orderExist = await Order.findOne({_id:id})
        if (!orderExist){
            return res.status(404).json({message : " Order Not Found. "})
        }
        await Order.findByIdAndDelete(id);
        res.status(201).json({message : " Order deleted Successfully."})
    } catch (error) {
        res.status(500).json({error : " Internal Server Error. "})
    }
}

Обратим внимание на fetch2 и fetch3. В них мы получаем активные и архивные заказы — для этого анализируем JSON на наличие в status записи «Заказ вручен! Спасибо за покупку».

Создание роутеров

Для того чтобы наши контроллеры могли работать, необходимо создать так называемый endpoint. 

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

Роутер для пользователя

Перейдите в папку routes, создайте файл userRoute.js и добавьте следующий код:

import express from "express"
import { create, deleteUser, fetch, update } from "../controller/userController.js";

const route = express.Router();

route.get("/getallusers", fetch)
route.post ("/create",create)
route.put("/update/:id", update)
route.delete("/delete/:id",deleteUser)

export default route;

Здесь мы импортируем express и наши функции из контроллера пользователя, объявляем переменную route и прописываем адреса. Обратите внимание, что для каждой функции — разный тип запроса (GET, POST, PUT, DELETE). Теперь для взаимодействия с контроллерами у нас есть конечные точки:

  • /getallusers — получение всех пользователей
  • /create — создание нового пользователя
  • /update/:id — обновление пользователя, в качестве аргумента мы принимаем id
  • /delete/:id — удаление пользователя, в качестве аргумента принимаем id

Роутер для продуктов

Создайте файл productRoute.js и добавьте следующий код:

import express from "express"
import { create, deleteProduct, fetch, fetch2, update } from "../controller/productController.js";

const route = express.Router();

route.get("/getallproducts", fetch)
route.get("/getproduct/:id", fetch2)
route.post ("/create",create)
route.put("/update/:id", update)
route.delete("/delete/:id",deleteProduct)

export default route;

Роутер для заказов

Создайте файл orderRoute.js и вставьте код:

import express from "express"
import { create, deleteProduct, fetch, fetch2, fetch3, update } from "../controller/orderController.js";

const route = express.Router();

route.get("/getallorders", fetch)
route.get("/getactiveorders", fetch2)
route.get("/getarchivedorders", fetch3)
route.post ("/create",create)
route.put("/update/:id", update)
route.delete("/delete/:id",deleteProduct)

export default route;

Создание главного файла 

После создания всех указанных файлов мы можем переходить к созданию главного файла нашей backend-части.

Создайте в корневой директории проекта файл index.js и импортируйте все необходимые модули:

import express from "express"
import mongoose from "mongoose"
import bodyParser from "body-parser"
import dotenv from "dotenv"

Подключение к базе данных

Для начала нам необходимо получить нашу базу данных, для удобства будем использовать облачные возможности, а именно DBaaS. Перейдем в панель управления Timeweb Cloud и откроем раздел «Базы данных».

Нажимаем на кнопку «Создать» в правом верхнем углу и выбираем MongoDB с конфигурацией: CPU 1 x 3.3ГГц, RAM 2 ГБ, NVMe 20 ГБ, 1 IPv4 стоимостью 600 ₽ в месяц.

Image2

Ждем установки базы данных и копируем данные для подключения из панели управления:

Image6

Настройка .env

Для безопасности необходимо использовать файл .env. Создаем файл и пишем в него следующее:

MONGO_URL = "mongodb://<user>:<password>@<ip>:27017/<db>?authSource=admin&directConnection=true"
  • <user> — логин пользователя
  • <password> — пароль пользователя
  • <ip> — публичный IP
  • <db> — название базы данных

Также добавим запись с портом, на котором будем запускать сервер:

PORT = 5001

Настройка подключения

Возвращаемся в файл index.js и пишем следующий код:

dotenv.config();

const PORT = process.env.PORT || 5000;
const MONGOURL = process.env.MONGO_URL; 

mongoose.connect(MONGOURL).then(()=>{
    console.log("Database connected Successfully.")
    app.listen(PORT, ()=>{
        console.log(`Server is running on port : ${PORT}`)
    })
}).catch(error => console.log(error));

Здесь:

  • Используем dotenv.config() для определения модуля dotenv и устанавливаем константы с данными из env-файла. 

  • Подключаемся к базе данных при помощи mongoose.connect(), где в качестве аргумента передаем ссылку для подключения. В случае удачного подключения выводим готовность базы данных и запускаем сервер на Express, а в случае появления ошибок выводим их в консоль и не даем запуститься серверу.

Подключение роутеров

Теперь для того, чтобы наши роутеры стали работать, подключим их к главному файлу нашего проекта. После подключения всех модулей добавим следующее:

import userRoute from "./routes/userRoute.js"
import productRoute from "./routes/productRoute.js"
import orderRoute from "./routes/orderRoute.js"

Далее объявим переменную для работы Express и активируем body-parser:

const app = express ();
app.use(bodyParser.json());

И в самый конец файла добавим начальные пути до наших роутов:

app.use("/api/user", userRoute)
app.use("/api/product", productRoute)
app.use("/api/order", orderRoute)

В итоге мы получаем следующее содержимое:

import express from "express"
import mongoose from "mongoose"
import bodyParser from "body-parser"
import dotenv from "dotenv"
import userRoute from "./routes/userRoute.js"
import productRoute from "./routes/productRoute.js"
import orderRoute from "./routes/orderRoute.js"


const app = express ();
app.use(bodyParser.json());

dotenv.config();

const PORT = process.env.PORT || 5000;
const MONGOURL = process.env.MONGO_URL; 

mongoose.connect(MONGOURL).then(()=>{
    console.log("Database connected Successfully.")
    app.listen(PORT, ()=>{
        console.log(`Server is running on port : ${PORT}`)
    })
}).catch(error => console.log(error));

app.use("/api/user", userRoute)
app.use("/api/product", productRoute)
app.use("/api/order", orderRoute)

Теперь, чтобы получить всех пользователей, мы используем путь /api/user/getallusers.

Запуск

Запускаем наше приложение при помощи команды:

npm start

Если всё сделано правильно, мы получим вывод:

Image9

Тестирование

Для проверки нашего RestAPI я рекомендую использовать Postman. Переходим на официальный сайт и скачиваем версию для своей ОС.

Image13

Открываем Postman и видим следующий интерфейс:

Image3

Нажимаем на плюс сверху экрана, и перед нами открывается страница, где мы можем отправлять запросы:

Image7

Пользователь

Напишем запрос для получения всех пользователей:

http://localhost:5001/api/user/getallusers

Отправим и получим:

Image14

Сервер нам пишет? что пользователей не найдено. Верно, нам необходимо их создать. Пишем адрес для создания пользователя:

http://localhost:5001/api/user/create

Мы определили этот запрос как POST, поэтому в контекстном меню выбираем POST:

Image16

Далее нам необходимо передать данные. Для этого переходим в body, выбираем тип raw и вставляем данные:

{
    "firstName": "Имя",
    "lastName": "Фамилия",
    "email": "example@example.ru",
    "phone": 800000000,
    "address": "Россия, г.Санкт-Петербург, улица Заставская, дом 22, к.2, лит. А, помещ. 303",
    "region" : "Ленинградская область",
    "city": "Санкт-Петербург",
    "postCode": "196006"
}

Image10

Теперь проверим созданных пользователей и получим:

Image8

Для обновления пользователя используем PUT, пишем адрес:

http://localhost:5001/api/user/update/{:id}

Вместо {:id} вставьте id пользователя.

В body вставим то же самое, что и для создания, но изменим фамилию с «Фамилия» на «Фамилия update». Отправим и получим:

Image17

И проверим адрес для удаления, используя тип DELETE и адрес:

http://localhost:5001/api/user/delete/{:id}

Вместо {:id} вставьте id пользователя.

Отправляем и получаем:

Image4

Теперь по аналогии проверим все остальные пути.

Продукты

Получение всех продуктов (GET):

http://localhost:5001/api/product/getallproducts

Получение продукта по id (GET):

http://localhost:5001/api/product/getproduct/{:id}

Создание (POST):

http://localhost:5001/api/product/create

В body указываем следующий JSON:

{
    "title": "Футболка OnlineStore",
    "subtitle": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Explicabo officia odit numquam eius et ipsa expedita sunt, voluptate dolorum quis, consequuntur molestias natus recusandae! Voluptate reiciendis nostrum consequatur libero odio?<br><br>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Explicabo officia odit numquam eius et ipsa expedita sunt, voluptate dolorum quis, consequuntur molestias natus recusandae! Voluptate reiciendis nostrum consequatur libero odio?",
    "article": "A264671",
    "brand": "Tutorial",
    "oldPrice": 1500,
    "price": 750,
    "path": ["Магазин", "Мужская одежда", "Футболки"],
    "specifications": {
        "color": {
            "perple": {
                "size": {
                    "XS": 10,
                    "S": 10,
                    "M": 10,
                    "L": 10,
                    "XL": 10,
                    "XXL": 10
                }
            }, 
            "black": {
                "size": {
                    "XS": 10,
                    "S": 10,
                    "M": 10,
                    "L": 10,
                    "XL": 10,
                    "XXL": 10
                }
            }, 
            "white": {
                "size": {
                    "XS": 10,
                    "S": 10,
                    "M": 10,
                    "L": 10,
                    "XL": 10,
                    "XXL": 10
                }
            }
        }
    },
    "images": ["url1", "url2", "url3"]
}

Обратим внимание на спецификации (цвет, размер), они сделаны так, что у каждого цвета футболки указан размер и количество товара с именно этими спецификациями

Обновление (PUT):

http://localhost:5001/api/product/update/{:id}

Удаление (DELETE):

http://localhost:5001/api/product/delete/{:id}

Заказы

Получение всех заказов (GET):

http://localhost:5001/api/order/getallorders

Получение активных заказов (GET):

http://localhost:5001/api/order/getactiveorders

Получение архивных заказов (GET):

http://localhost:5001/api/order/getarchivedorders

Создание (POST):

http://localhost:5001/api/order/create

И в body укажем следующее:

{
    "firstName": "Имя",
    "lastName": "Фамилия",
    "email": "example@example.ru",
    "phone": 800000000,
    "address": "Россия, г.Санкт-Петербург, улица Заставская, дом 22, к.2, лит. А, помещ. 303",
    "region" : "Ленинградская область",
    "city": "Санкт-Петербург",
    "postCode": "196006",
    "products": {
        "Футболка Online Store":{
            "id": "681fb35b1cad84bc5b9c8d6a",
            "specification": {
                "perple": {
                    "size": "XL"
                }
            },
            "count": 2
        }
    },
    "status" : {
        "Заказ создан": "12.04.2025, 18:00"
    },
    "price": 1000
}

Обновление (PUT):

http://localhost:5001/api/order/update/{:id}

Удаление (DELETE):

http://localhost:5001/api/order/delete/{:id}

Дополнительные функции

После заказа на указанную почту необходимо отправить уведомление о том, что заказ создан. Так как у нас магазин, то мы отправим чек. Для этого воспользуемся простым HTML-шаблоном:

Image15

Так как заказ должен отправляться на электронную почту, мы воспользуемся зависимостью nodemailer и почтой от Timeweb Cloud. 

Для почты нам нужен домен. Переходим в панель управления, в меню выбираем «Домены и SSL» и нажимаем кнопку «Купить домен».

Сейчас необходимо определиться с тем, как наш сайт будет находится в сети, поэтому к этому этапу необходимо отнестись серьезно. 

После покупки домена убедитесь, что он появился у вас в панели управления и перейдите на страницу «Почта» и выберите «Создать ящик».

Image11

После создания почты вернитесь в IDE, и запишите в .env-файл данные от почты:

MAIL_SERVICE = "smtp.timeweb.ru"
MAIL_USER = "<email>"
MAIL_PASSWORD = "<password>"

Далее перейдите в папку function и создайте файл createCheque.js:

Импортируйте модули:

import nodemailer from "nodemailer";
import dotenv from "dotenv";
import axios from "axios";

Настройте подключение .env-файла:

dotenv.config();
const service = process.env.MAIL_SERVICE
const user = process.env.MAIL_USER
const password = process.env.MAIL_PASSWORD

Настройте nodemailer:

let transporter = nodemailer.createTransport({
    host: service,
    port: 587,
    secure: false,
    auth: {
        user: user,
        pass: password
    }
});

Далее создадим функцию для получения стоимости товара:

async function fetchProductPrice(id) {
    try {
        
      const response = await axios.get(`<server_address>/api/product/getproduct/${id}`); 
      return response.data.price;
    } catch (err) {
      console.error(`Ошибка при получении цены товара: ${err.message}`);
      throw err;
    }
  }

Обратите внимание, что в этой строке: 

const response = await axios.get(`<server_address>/api/product/getproduct/${id}`); 

— вместо <server_address> надо написать адрес нашего сервера, для локального тестирования это http://localhost:5001.

Функция для обработки данных товара:

async function formatProduct(productKey, productValue) {
    const name = productKey;
    const specification = productValue.specification;
    const color = Object.keys(specification)[0];
    const size = specification[color].size;
    const count = productValue.count;
    const pricePerItem = await fetchProductPrice(productValue.id);
  
    return `
      <p class="amount">
        ${name}, ${color}, ${size}, количество: ${count}
        <span>${pricePerItem * count} руб.</span>
      </p>
    `;
  }

Функция для создания HTML-кода:

async function generateHTML() {
  const formattedProducts = [];

  for (const [key, value] of Object.entries(data.products)) {
    const formattedProduct = await formatProduct(key, value);
    formattedProducts.push(formattedProduct);
  }
  
  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Bill</title>
      <style>
    body{
      max-width: 30%;
      padding: 1.5%;
      border: 1px solid black;
    }
      .header img{
        position: absolute;
        width: 50px;
        top: 20px;
        left: 20px;
      }
      .header{
        text-align: center;
      }
      .no-margin{
        margin: 0;
      }
      .left{
        float: left;
      }
      .right{
        float: right;
      }
      .roow{
        margin: -2px;
      }
      .margin-20{
        margin-right: 20px;
      }
      .centre{
        text-align: center;
      }
      .clearfix{
        clear: both;
      }
      .fees-content{
        padding: 30px 0px 30px 0px;
      }
      .amount span{
        float: right;
      }

      .signarea p{
        text-align: right;
        margin-top: 60px;
      }

    </style>
    </head>
    <body>
    <div class="body">
      <div class="header">
        <h3 class="no-margin">OnlineStore</h3>
        <h4 class="no-margin">Тестовый магазин для статьи на TimewebCloud</h4>
      </div>
      <div class="content">
        <div class="roow">
          <div class="left">
            <p>Номер заказа: ${data._id}</p>
          </div>
          <div class="right">
            <p class="margin-20">Дата: ${new Date(data.createdAt).toLocaleDateString('ru-RU')}</p>
          </div>
        </div>
        <div class="clearfix"></div>
        <div class="centre">
          <h3>Данные покупателя</h3>
        </div>
        <div>
          <p>Имя: ${data.firstName} ${data.lastName}</p>
          <p>Компания: ${data.company || ''}</p>
        </div>
        <hr/>
        <div class="centre">
          <h3>Содержимое заказа</h3>
        </div>
        <div class="fees-content">
          <!-- Продукты -->
          ${formattedProducts.join('')}
        </div>
        <hr/>
        <div>
          <p class="amount">Итого
            <span>${data.price}</span>
          </p>
        </div>
      </div>
    </div>
    </body>
    </html>
    `;
    return html
  }

Код для отправки письма:

generateHTML().then(html => {
    let mailOptions = {
      from: '"Online-Store" <username>',
      to: recipient,
      subject: 'Спасибо за заказ',
      html: html
  };
  
  transporter.sendMail(mailOptions, function(error, info){
      if (error) {
          return console.log(error);
      }
      console.log('Сообщение успешно отправлено: %s', info.messageId);
  });
  }).catch(error => {
    console.error(error);
  });

Обратите внимание, что в строке:

from: '"Online-Store" <username>' 

— вместо <username> необходимо написать ваш почтовый адрес, с которого будет производиться отправка писем.

И в завершение для получения данных из JSON для чека обернем все созданные функции в функцию:

export default function sendEmail(data)

Данную функцию мы будем вызывать из контроллера, поэтому добавили к ней экспорт.

Переходим в файл controller/orderController.js и добавляем в начало после импорта модели импорт ранее созданного файла:

import sendEmail from "../function/createCheque.js"

Добавим в create строчку кода, которая будет вызывать отправку письма. Обновленный код:

export const create = async(req, res)=>{
    try {
        const orderData = new Order( req.body);
        const {createdAt} = orderData;
        const orderExist = await Order.findOne({createdAt})
        if (orderExist){
            return res.status(400).json({message : "Order already exist."})
        }
        
        sendEmail(orderData)
        const savedOrder = await orderData.save();
        res.status(200).json(savedOrder)
        
    } catch (error) {   
        console.log(error);
        
        res.status(500).json({error : "Internal Server Error. "})
    }
}
Полный код файла createCheque.js
import nodemailer from "nodemailer";
import dotenv from "dotenv"
import axios from "axios";

dotenv.config();
const service = process.env.SERVICE
const user = process.env.USER
const password = process.env.PASSWORD

let transporter = nodemailer.createTransport({
    host: service,
    port: 587,
    secure: false,
    auth: {
        user: user,
        pass: password
    }
});

export default function sendEmail(data) {
  const recipient = data.email;
  async function fetchProductPrice(id) {
    try {
        
      const response = await axios.get(`http://localhost:5001/api/product/getproduct/${id}`); 
      return response.data.price;
    } catch (err) {
      console.error(`Ошибка при получении цены товара: ${err.message}`);
      throw err;
    }
  }
  
  async function formatProduct(productKey, productValue) {
    const name = productKey;
    const specification = productValue.specification;
    const color = Object.keys(specification)[0];
    const size = specification[color].size;
    const count = productValue.count;
    const pricePerItem = await fetchProductPrice(productValue.id);
  
    return `
      <p class="amount">
        ${name}, ${color}, ${size}, количество: ${count}
        <span>${pricePerItem * count} руб.</span>
      </p>
    `;
  }
  async function generateHTML() {
  const formattedProducts = [];

  for (const [key, value] of Object.entries(data.products)) {
    const formattedProduct = await formatProduct(key, value);
    formattedProducts.push(formattedProduct);
  }
  
  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Bill</title>
      <style>
    body{
      max-width: 30%;
      padding: 1.5%;
      border: 1px solid black;
    }
      .header img{
        position: absolute;
        width: 50px;
        top: 20px;
        left: 20px;
      }
      .header{
        text-align: center;
      }
      .no-margin{
        margin: 0;
      }
      .left{
        float: left;
      }
      .right{
        float: right;
      }
      .roow{
        margin: -2px;
      }
      .margin-20{
        margin-right: 20px;
      }
      .centre{
        text-align: center;
      }
      .clearfix{
        clear: both;
      }
      .fees-content{
        padding: 30px 0px 30px 0px;
      }
      .amount span{
        float: right;
      }

      .signarea p{
        text-align: right;
        margin-top: 60px;
      }

    </style>
    </head>
    <body>
    <div class="body">
      <div class="header">
        <h3 class="no-margin">OnlineStore</h3>
        <h4 class="no-margin">Тестовый магазин для статьи на TimewebCloud</h4>
      </div>
      <div class="content">
        <div class="roow">
          <div class="left">
            <p>Номер заказа: ${data._id}</p>
          </div>
          <div class="right">
            <p class="margin-20">Дата: ${new Date(data.createdAt).toLocaleDateString('ru-RU')}</p>
          </div>
        </div>
        <div class="clearfix"></div>
        <div class="centre">
          <h3>Данные покупателя</h3>
        </div>
        <div>
          <p>Имя: ${data.firstName} ${data.lastName}</p>
          <p>Компания: ${data.company || ''}</p>
        </div>
        <hr/>
        <div class="centre">
          <h3>Содержимое заказа</h3>
        </div>
        <div class="fees-content">
          <!-- Продукты -->
          ${formattedProducts.join('')}
        </div>
        <hr/>
        <div>
          <p class="amount">Итого
            <span>${data.price}</span>
          </p>
        </div>
      </div>
    </div>
    </body>
    </html>
    `;
    return html
  }
  generateHTML().then(html => {
    let mailOptions = {
      from: '"Online-Store" <tutorial@testmailtw.ru>',
      to: recipient,
      subject: 'Спасибо за заказ',
      html: html
  };
  
  transporter.sendMail(mailOptions, function(error, info){
      if (error) {
          return console.log(error);
      }
      console.log('Сообщение успешно отправлено: %s', info.messageId);
  });
  }).catch(error => {
    console.error(error);
  });
    
}

Сохраните и проверьте что сервер запущен.

Перейдите в Postman и отправьте запрос на создание заказа. Проверьте, что в поле email находится действительный адрес почты. Если заказ успешно создан, переходим в почту и видим письмо:

Image12

Заключение

В этой статье мы познакомились с MongoDB, научились создавать базы данных и взаимодействовать с ними из Node.js при помощи Mongoose и Express, а также создали RestAPI для нашего интернет магазина. 

Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server
02 июня 2025 г.
34
38 минут чтения
Средний рейтинг статьи: 5
Пока нет комментариев