Настройка экспресс-приложения с контроллерами и маршрутизацией

Создано: May 11, 2022 2:41 AM
Опубликовано: Да

Настройка

Мы инициализируем наш проект node в выбранной нами директории, создадим директорию сервера и файл server.js для нашего сервера node

npm init -y
mkdir server
touch server/server.js
Входим в полноэкранный режим Выходим из полноэкранного режима

Мы устанавливаем babel и webpack dev-зависимости для пакетирования и минификации нашего кода для возможного производства

npm install -D @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-node-externals
Войдите в полноэкранный режим Выход из полноэкранного режима

💡 webpack-node-externals не позволяет нам подключать модули из папки node_modules к нашему коду, так как это сделает его раздутым. Webpack будет загружать модули из папки node_modules и подключать их. Это хорошо для кода фронтенда, но модули бэкенда обычно не готовы к этому. Подробнее здесь

Затем мы настроим webpack, создав webpack.config.js в корне каталога нашего проекта.

const path = require('path');
const CURRENT_WORKING_DIR = process.cwd();
const nodeExternals = require('webpack-node-externals');

const config = {
    name: "server",
    entry: [ path.join(CURRENT_WORKING_DIR, '/server/server.js')],
    target: "node",
    output: {
        path: path.join(CURRENT_WORKING_DIR, '/dist/'),
        filename: "server.generated.js",
        publicPath: '/dist/',
        libraryTarget: "commonjs",
    },
    externals: [nodeExternals()],
    module: {
        rules: [
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            }
        ]
    }
};

module.exports = config;
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы настраиваем babel, создавая файл .babelrc в корне нашего проекта

{
  "presets": [
    ["@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }]
  ]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Что такое пакетирование с помощью webpack?

Демистификация пакетирования кода с помощью модулей JavaScript

Далее мы установим express, чтобы начать сборку нашего сервера

npm install express
Вход в полноэкранный режим Выход из полноэкранного режима

Мы установим следующие пакеты в качестве промежуточного программного обеспечения express, чтобы придать нашему серверу расширенную функциональность

body-parser: Для разбора тела входящего запроса как json и urlencoded значений (также работает для FormData). Тело запроса будет доступно для наших обработчиков.

cookie-parser: Разбирает cookies в заголовках запроса и делает их доступными в объекте запроса для наших обработчиков.

шлем: Для добавления необходимых заголовков безопасности на наш сервер для повышения безопасности. Это необходимо для защиты от основных атак, таких как атаки межсайтового скриптинга. Сам по себе он не является достаточным, но полезен для обеспечения некоторого базового уровня безопасности

сжатие: Для сжатия тела ответа

cors: Для настройки политики разделения ресурсов (Cross Origin Resource Sharing), чтобы клиенты из разрешенных доменов могли взаимодействовать с нашим сервером. Хорошую статью о кросс-оригинальных запросах можно посмотреть здесь

npm install body-parser cookie-parser helmet compression cors
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы можем создать наше экспресс-приложение в файле express.js в каталоге сервера

import express from "express";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compress from "compression";
import cors from "cors";

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
app.use(helmet());
app.use(compress());
app.use(cors());

export default app;
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы экспортируем наше экспресс-приложение для импорта в наш основной файл server.js. Мы создаем файл config.js для хранения необходимых конфигураций, которые будут использоваться на нашем сервере, и экспортируем один объект config для импорта в server.js.

export default {
    env: process.env.NODE_ENV || 'development',
    port: process.env.PORT || 4000
}
Вход в полноэкранный режим Выход из полноэкранного режима
import mongoose from "mongoose";
import config from './config/config'

app.listen(config.port, (err) => {
    if (err) {
        console.error(err);
    } else {
        console.info(`Application running on PORT: ${config.port}`)
    }
})
Войти в полноэкранный режим Выход из полноэкранного режима

Когда наши серверные файлы настроены, мы можем запустить наш dev-сервер. Nodemon является полезным ресурсом для запуска вашего dev-сервера, поскольку он автоматически перезапускается (что-то вроде горячей перезагрузки), когда вы вносите изменения в исходный код.

npm install -D nodemon
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем мы создаем конфигурационный файл nodemon nodemon.json и настраиваем его на запуск webpack и нашего сгенерированного кода из директории dist, которую мы указали в webpack.config.js.

{
"verbose": false,
"watch": ["./server"],
"exec": "webpack --mode=development --config webpack.config.server.js && node ./dist/server.generated.js"
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Наконец, мы установили dev скрипт в нашем package.json для запуска nodemon с вышеуказанным конфигом, чтобы настроить запуск нашего dev сервера

{
  "name": "social-media-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon", //. <-- HERE
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.2",
    "compression": "^1.7.4",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "express": "^4.17.3",
    "helmet": "^5.0.2",
  },
  "devDependencies": {
    "@babel/core": "^7.17.5",
    "babel-loader": "^8.2.3",
    "nodemon": "^2.0.15",
    "webpack": "^5.69.1",
    "webpack-cli": "^4.9.2",
    "webpack-node-externals": "^3.0.0"
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем запустить скрипт dev для запуска нашего сервера

npm run dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Вывод ниже показывает, что наш код был собран webpack и сгенерированный файл в настоящее время выполняется, а сервер запущен.

Теперь мы можем приступить к реальной работе

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

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

Мы будем использовать Prisma в качестве объектно-реляционного сопоставителя для нашей базы данных MongoDB. Mongoose также является другим вариантом, но мне нравится безопасность типов Prisma при использовании typescript и декларации схем, поскольку это упрощает обслуживание.

Команда Prisma подготовила инструкцию по настройке Prisma с MongoDB, которую можно найти здесь:

Начать с нуля с MongoDB (15 мин)

Мы можем проделать все шаги для начала настройки до установки схемы. Ниже приведена схема для нашей модели пользователя

type UsersImage {
  contentType String
  data        Bytes
}

model User {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  v               Int         @map("__v") @default(0)
  createdAt       DateTime    @default(now()) @db.Date
  email           String      @unique
  password        String
  image           UsersImage?
  name            String
  salt            String
  updatedAt       DateTime    @updatedAt @db.Date
  about String?

  @@map("users")
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы определяем имя поля в первом столбце, тип поля в следующем и атрибуты и модификаторы поля в последнем столбце

  • id: Первичный ключ для каждого пользовательского документа. Это будет строка, обозначаемая как первичный ключ с помощью @id. Модификатор @default(auto()) автоматически генерирует ID по умолчанию при создании документа. @map позволяет нам задать имя поля в базе данных. В MongoDB имя поля ID всегда _id и должно быть отображено с помощью @map("_id"). Когда мы создаем документы и просматриваем их в базе данных, мы будем видеть _id как поле, а когда мы используем Prisma ORM в нашей разработке, мы сможем получить к нему доступ как к id на объекте пользователя. Prisma позаботится о сопоставлении. @db.ObjectId — это родной атрибут типа базы данных, который позволяет нам указать тип данных для поля. ObjectIds — это тип поля, родной для документов MongoDB, подобно тому, как в базах данных SQL есть такие типы, как VARCHAR.
  • v: Это поле в документе предназначено для определения версии документа. В будущем, если мы начнем сохранять разные версии документов, но не захотим выбрасывать старые данные, это поле позволит нам различать, какую версию документов использовать. Подробнее о шаблоне версионирования документов можно прочитать здесь. Мы установили его версию по умолчанию равной 0
  • createdAt и updatedAt: Мы храним даты создания и обновления документов.
  • email: Мы храним электронную почту пользователя и обеспечиваем ее уникальность
  • пароль: хэшированный пароль пользователя, который будет храниться
  • изображение: На данный момент мы будем хранить данные изображения пользователя в базе данных как двоичные данные. Обратите внимание, что это не лучшая практика, и позже мы будем работать над переносом схемы для хранения ссылок на изображения, которые будут извлекаться из удаленного хранилища. Мы создаем поле type для определения формы нашего изображения. Мы будем хранить данные и тип содержимого (это тип MIME)
  • имя: Имя пользователя как строка
  • about: Краткое описание пользователя
  • salt (соль): Это часть случайных данных, используемых при шифровании пароля пользователя во время аутентификации. Имея разные значения соли для разных пользователей, мы повышаем безопасность наших паролей, так как это ограничивает возможность использования общих паролей. Страница Salting в Википедии — отличное место для ознакомления с преимуществами использования соли.

Полный справочник полей и модификаторов Prisma можно найти здесь.

После определения нашей модели мы можем приступить к установке клиента Prisma

npm install @prisma/client
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем мы запускаем prisma generate, который считывает нашу схему и генерирует версию клиента Prisma (с определениями типов и безопасностью), адаптированную к нашей схеме.

prisma generate
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы включить использование prisma, нам нужно инициализировать prisma client, который позволит нам запрашивать нашу базу данных.

touch server/prisma/prisma.js
Вход в полноэкранный режим Выход из полноэкранного режима
import {PrismaClient} from '@prisma/client'

const prisma = global.prisma || new PrismaClient()

if (process.env.NODE_ENV === 'development') {
    if (!global.prisma) {
        global.prisma = prisma
        global.prisma.$connect().then(() => {
            console.log('Database connected successfully')
        });
    }
} else {
    prisma.$connect().then(() => {
        console.log('Database connected successfully')
    });
}

export default prisma;
Войти в полноэкранный режим Выход из полноэкранного режима

Мы импортируем PrismaClient, подключаемся к базе данных и экспортируем объект prisma для использования в нашем приложении. Во время разработки обычно приходится перезапускать сервер много раз по мере внесения изменений, поэтому, чтобы избежать создания многочисленных соединений, мы прикрепляем объект prisma к глобальному объекту узла (известному как globalThis). Мы проверяем, существует ли этот объект на глобальном объекте, когда приложение запускается, или создаем нового клиента в противном случае. Обратите внимание, что присвоение объекта global выполняется только в процессе разработки.

После этого мы можем приступить к запросам к базе данных.

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

touch server/controllers/user.controller.js
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте определим метод для создания и хранения пользователей в базе данных. Это будет полезно в процессе регистрации

import crypto from 'crypto';
import isEmail from 'validator/lib/isEmail';
import contains from 'validator/lib/contains';
import prisma from '../prisma/prisma';
import {ValidationError} from "../helpers/db.error.handler";

const encryptPassword = (password, salt) => {
    try {
        return crypto.createHmac('sha1', salt).update(password).digest('hex');
    } catch (err) {
        console.error(err);
    }
}

const makeSalt = () => String(Math.round((new Date().valueOf() * Math.random())))

const emailIsValid = (email) => {
    return isEmail(email)
}

const nameIsValid = (name) => {
    return name.length > 1
}

const passwordIsValid = (password) => {
    const errors = [];
    if (!password) {
        errors.push('Password is required')
    }
    if (password?.length < 8) {
        errors.push('Password must be at least 8 characters')
    }
    if (password && contains(password, 'password')) {
        errors.push('Password should not contain the word password')
    }

    if (errors.length > 0) {
        return {
            isValid: false,
            message: errors.join(',')
        }
    } else {
        return {
            isValid: true
        }
    }
}

const store = async (req, res, next) => {
    const {name, email, password} = req.body;
    const userData = {name, email, password};
    try {
                const errors = {};

            if (!nameIsValid(name)) {
                errors.name = 'User name too short'
            }

            if (!emailIsValid(email)) {
                errors.email = 'Invalid email'
            }

                const existingUser = await prisma.findUnique({
            where: {
                email
            }
            })

            if (existingUser !== null) {
                 throw new ValidationError({
            error: "User already exists"
            })
            }

            const passwordValid = passwordIsValid(password)

            if (!passwordValid.isValid) {
                errors.password = passwordValid.message
            }

            if (Object.keys(errors).length > 0) {
                throw new ValidationError(errors);
            }

            const salt = makeSalt();

            const hashed_password = encryptPassword(password, salt);

            await prisma.create({
                data: {
                    name,
                    email,
                    password: hashed_password,
                    salt,
                }
            })
        return res.status(200).json({
            message: 'User saved successfully'
        })
    } catch (e) {
        if (e instanceof ValidationError) {
            return res.status(422).json(e.errors)
        }
        next(e);
    }
};

export default { store }
Войти в полноэкранный режим Выход из полноэкранного режима

Давайте разберем вышеописанное. Мы создали метод store, который представляет собой промежуточное ПО Express для обработки создания пользователей при отправке запроса на этот маршрут. Он принимает в качестве параметра объект входящего запроса, а также объект для отправки ответа. Функция next полезна, если мы собираемся передать ошибку обработчику ошибок или передать управление следующему промежуточному ПО. В данном случае мы будем использовать next для передачи любых неожиданных ошибок нашему обработчику ошибок. О том, как работает express, вы можете узнать из его документации.

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

nameIsValid проверяет, что в имени пользователя больше одного символа.

emailIsValid использует вспомогательный метод isEmail из пакета validator для проверки того, что предоставленный email является допустимым форматом email.

Мы также проверяем, существует ли уже пользователь, и в этом случае сразу же выдаем ошибку

passwordIsValid проверяет, что пароль присутствует, состоит как минимум из 8 символов и не содержит слова «password». Мы храним отдельные сообщения об ошибках в массиве, объединяем их и возвращаем в качестве свойства message объекта вывода, а также isValid для обозначения того, является ли пароль действительным.

Если какая-либо из этих проверок не прошла, имена полей добавляются в объект ошибки. На основании количества ключей в объекте ошибки после валидации мы бросаем ValidationError с ошибками как часть конструктора. Мы создаем ValidationError, как показано ниже.

touch server/helpers/db.error.handler.js
Вход в полноэкранный режим Выход из полноэкранного режима
export class ValidationError extends Error {
    constructor(errors) {
        super();
        this.errors = errors;
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы проверяем наличие этой ошибки в блоке catch нашего try catch и на основании этого отправляем код состояния 422 и объект ошибок в качестве ответа.

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

Наконец, мы используем клиент prisma для создания пользователя и отправляем ответ 200 с сообщением пользователю. Если возникает ошибка, которая не является ValidationError, мы передаем ее нашему обработчику ошибок пользователю next(e) Мы рассмотрим этот обработчик ошибок в ближайшее время. Наконец, мы экспортируем метод store как часть экспорта объекта по умолчанию. Это позволит другим модулям иметь к нему доступ.

Маршрутизация

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

import express from 'express';
import userController from '../controllers/user.controller';

const userRouter = express.Router();

userRouter.route('/api/users')
    .post(userController.store)

export default userRouter
Вход в полноэкранный режим Выход из полноэкранного режима

Выше мы:

  1. Импортируем модуль express и экспортированный объект userController.
  2. Создаем маршрутизатор express для обработки маршрутизации
  3. Маршрутизируйте POST запросы к /api/users и обрабатывайте их с помощью метода store, который мы создали
  4. Экспортируйте userRouter.

Наш последний шаг — «прикрепить» наш userRouter к нашему серверу

import express from "express";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compress from "compression";
import cors from "cors";
import userRouter from "./routes/user.routes"; //new

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
app.use(helmet());
app.use(compress());
app.use(cors());
app.use(userRouter); //new

export default app;
Вход в полноэкранный режим Выйти из полноэкранного режима

Когда наш маршрутизатор подключен к серверу, мы можем запустить наш сервер, чтобы начать принимать запросы

npm run dev
Вход в полноэкранный режим Выход из полноэкранного режима

Используя post man, мы делаем POST запрос на наш сервер (работающий на порту 4000, помните) по маршруту /api/users для создания пользователя.

Мы получаем успешный ответ от сервера

Мы можем проверить нашу базу данных, чтобы увидеть, что пользователь успешно создан

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

Листинг, удаление и обновление — это дополнительные функции, которые мы можем включить в наш сервер, создавая больше сервисов, контроллеров и маршрутов, а также отправляя соответствующие запросы к базе данных Mongo с помощью клиента Prisma. Документация Prisma полна подробностей о том, как это сделать, и вы можете ознакомиться с ними, чтобы расширить настройки и создать простой CRUD API

Оцените статью
devanswers.ru
Добавить комментарий