Постановка задачи
Необходимо собрать базовый шаблон RESTful backend приложения на NodeJS + Express, который:
-
легко документируется
-
просто наполняется функционалом
-
позволяет легко настраивать защиту маршрутов
-
имеет простую встроенную автоматическую валидацию
Гайд достаточно обширный, поэтому сначала мы разберем и реализуем различные части, а затем соберем приложение воедино. Готовый репозиторий можно посмотреть на Github.
Набор инструментов
Сердце нашего приложения – спецификация OpenApi 3.0. В нашем случае это описание API на языке разметки YAML, которое позволит автоматически генерировать и защищать маршруты и документировать API.
Для простоты возьмем MongoDB и mongoose, в целом ничего не помешает заменить эту связку на любую другую в своём шаблоне.
Passport.js – защита маршрутов, аутентификация и авторизация. Стратегия passport-jwt. Мы будем использовать jwt-access и refresh токены.
Первоначальная настройка
Инициализируем проект, запустив npm init или yarn init, я предпочитаю yarn.
Для начала стоит позаботиться об удобности разработки, стиле кода и допущениях.
За стиль кода у меня отвечают eslint и prettier.
В корне создаем конфиги для eslint и prettier. Для удобства разработки и сборки я использую nodemon, npm-run-all, rimraf, babel. Ниже мои настройки:
.eslintrc.json
{ "env": { "node": true, "es2021": true }, "extends": [ "eslint:recommended", "airbnb-base", "prettier" ], "plugins": [ "prettier" ], "rules": { "no-console": 0, "prettier/prettier": ["error"], "import/extensions": 0, "import/prefer-default-export": "off", "import/no-unresolved": 0, "no-duplicate-imports": ["error", { "includeExports": true }], "react/prop-types": 0, "no-underscore-dangle": 0, "no-param-reassign": ["error", { "props": false }], "no-case-declarations": 0, "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], "space-infix-ops": ["error", { "int32Hint": false }], "no-unused-vars": ["error", { "argsIgnorePattern": "next" }] } }
.prettierrc
{ "printWidth": 100, "singleQuote": true, "tabWidth": 4, "bracketSpacing": true, "endOfLine": "lf", "semi": true, "trailingComma": "none" }
Добавьте в свой package.json
"dependencies": { "@babel/node": "^7.13.13", "body-parser": "^1.19.0", "connect": "^3.7.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", "express-openapi-validator": "^4.12.6", "jsonwebtoken": "^8.5.1", "mongoose": "^5.12.2", "morgan": "^1.10.0", "passport": "^0.4.1", "passport-jwt": "^4.0.0", "swagger-routes-express": "^3.3.0", "swagger-ui-express": "^4.1.6", "uuid": "^8.3.2", "validator": "^13.5.2", "yamljs": "^0.3.0" }, "devDependencies": { "@babel/cli": "^7.13.14", "@babel/core": "^7.13.14", "@babel/preset-env": "^7.13.12", "eslint": "^7.23.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.1.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.3.1", "nodemon": "^2.0.7", "npm-run-all": "^4.1.5", "prettier": "^2.2.1", "rimraf": "^3.0.2" }, "babel": { "presets": [ "@babel/preset-env" ] }, "scripts": { "transpile": "babel ./src --out-dir bin --copy-files", "clean": "rimraf bin", "build": "npm-run-all clean transpile", "server": "node ./bin/app.js", "dev": "npm-run-all build server", "start": "yarn dev", "watch": "nodemon" }
Создайте nodemon.json в корне
{ "watch": ["src/*"], "ext": "js, json, yaml", "exec": "yarn run dev" }
Установите зависимости, запустив npm или yarn.
Немного про безопасность
Я подготовил несколько диаграмм, чтобы пошагово разобрать подход, который мы реализуем. На всякий случай собрал их в PDF.
Логика такая:
-
При успешной аутентификации клиент получает JWT access токен и защищённый http-only куки с refresh токеном.
-
При каждом запросе клиент проверяет, не истек ли срок жизни токена доступа (JWT access token), если все ок, вставляет его в заголовок «Authorization», если токен истек, то сначала запрашивает новый токен доступа. Ниже более подробно про наш бэкенд.
Регистрация пользователя

-
На Backend передаем в открытом виде(но только по HTTPS) e-mail, пароль, какие-то дополнительные данные, которые вам нужны (nickname для примера на диаграмме)
-
Генерируем уникальную соль и хэшируем пароль с этой солью, после чего записываем в базу
Аутентификация

-
Клиент передает по HTTPS email и пароль
-
Пытаемся получить пользователя из базы
-
Получаем либо пользователя, либо undefined
-
Если undefined возвращаем сообщение об ошибке, и не говорим неверна почта или пароль
-
Берем из базы соль пользователя, хэшируем с этой солью введенный пользователем пароль и сверяем с сохраненным в базе хэшем. Если пароль введен неверно, возвращаем сообщение об ошибке, аналогично пункту 4
-
Если пароль введен верно, генерируем JWT токен доступа, с коротким сроком жизни и Refresh токен, который нужен для получения нового токена доступа, с более длительным сроком жизни. Это не показано на диаграмме, но refresh токен записывается в базу как одно из полей пользователя.
-
Возвращаем пользователю JWT токен доступа и устанавливаем HTTP-only cookie (secure, т.к. у нас HTTPS).
Обновление JWT токена доступа

-
Обращаемся на маршрут обновления токена доступа
-
Получаем из HTTP-only cookie refresh токен
-
Если refresh токена нет – возвращаем ошибку
-
Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.
-
Ищем пользователя по refresh токену.
-
Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.
-
Если пользователь найден, то генерируем новую пару JWT токена доступа и refresh токена. Записываем refresh в базу
-
И как в предыдущем разделе передаем токен доступа и записываем refresh токен в cookie
Выход из системы

-
Переход на маршрут выхода из системы. Внимание на диаграмме ошибка, маршрут что-то типа /user/logout
-
Получаем из HTTP-only cookie refresh токен
-
Если refresh токена нет – возвращаем ошибку
-
Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.
-
Ищем пользователя по refresh токену.
-
Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.
-
Удаляем у пользователя из базы refresh токен
-
Сбрасываем cookie у клиента
Вспомогательные модули безопасности
Создайте следующую файловую структуру в корне проекта:

Для работы необходимо подготовить:
-
SSL сертификат и закрытый ключ к нему
-
Закрытый и публичный ключи для генерации JWT токена доступа
-
Закрытый и публичный ключи для генерации JWT refresh токена. На самом деле для реализации refresh токена достаточно генерации уникальной строки, можно использовать uuid, например, но я не ищу легких путей.
Если у вас нет SSL сертификата, можно сгенерировать свой, но использовать такой сертификат в боевом проекте не стоит, так как к self-signed сертификатам нет доверия.
Итак для генерации SSL сертификата и закрытого ключа можно воспользоваться openssl:
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout ssl.key -out ssl.crt
Генерируем ключи для JWT:
ssh-keygen -t rsa -b 4096 -m PEM -f jwtPrivate.key openssl rsa -in jwtPrivate.key -pubout -outform PEM -out jwtPublic.pem ssh-keygen -t rsa -b 4096 -m PEM -f refreshPrivate.key openssl rsa -in refreshPrivate.key -pubout -outform PEM -out refreshPublic.pem
Все ключи и сертификаты складываем в ./src/crypto/
Напишем несколько вспомогательных модулей:
./src/utils/cryptoHelper.js
import crypto from 'crypto'; /** * Валидация пароля */ export function validatePassword(password, hash, salt) { const hashCandidate = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex'); return hash === hashCandidate; } /** * Генерация соли и хэша пароля */ export function genHashWithSalt(password) { const salt = crypto.randomBytes(32).toString('hex'); const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex'); return { salt, hash }; }
./src/utils/jwtHelper.js
import fs from 'fs'; import path from 'path'; import jsonwebtoken from 'jsonwebtoken'; // настраиваем пути и читаем ключи const jwtPrivate = path.join(__dirname, '../crypto/', 'jwtPrivate.pem'); const refreshPrivate = path.join(__dirname, '../crypto/', 'refreshPrivate.pem'); const refreshPublic = path.join(__dirname, '../crypto/', 'refreshPublic.pem'); const JWT_PRIV_KEY = fs.readFileSync(jwtPrivate, 'utf8'); const REFRESH_PRIV_KEY = fs.readFileSync(refreshPrivate, 'utf8'); const REFRESH_PUBLIC_KEY = fs.readFileSync(refreshPublic, 'utf8'); // выпуск JWT токена доступа export function issueJWT(user) { const { _id } = user; const expiresIn = '10m'; const payload = { uid: _id, iat: Math.floor(Date.now() / 1000) }; const signedToken = jsonwebtoken.sign(payload, JWT_PRIV_KEY, { expiresIn, algorithm: 'RS256' }); return { token: `Bearer ${signedToken}`, expires: expiresIn }; } //выпуск JWT refresh токена export function issueRefresh(user) { const { _id } = user; const expiresIn = '7d'; const payload = { uid: _id, iat: Math.floor(Date.now() / 1000) }; const signedToken = jsonwebtoken.sign(payload, REFRESH_PRIV_KEY, { expiresIn, algorithm: 'RS256' }); return { token: signedToken, expires: expiresIn }; } //валидация refresh токена export function isValidRefresh(token) { try { jsonwebtoken.verify(token, REFRESH_PUBLIC_KEY, { algorithm: 'RS256' }); } catch (error) { return false; } return true; }
./src/utils/passport.js
import { Strategy, ExtractJwt } from 'passport-jwt'; import fs from 'fs'; import path from 'path'; import mongoose from 'mongoose'; import { userSchema } from '../db/models/User'; const User = mongoose.model('User', userSchema); const pathToKey = path.join(__dirname, '../crypto/', 'jwtPublic.pem'); const PUB_KEY = fs.readFileSync(pathToKey, 'utf8'); const options = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: PUB_KEY, algorithms: ['RS256'] }; export const strategy = (pass) => { pass.use( new Strategy(options, (jwtPayload, done) => { User.findOne({ _id: jwtPayload.uid }, (err, user) => { if (err) { return done(err, false); } if (user) { return done(null, user); } return done(null, null); }); }) ); };
Это описание jwt стратегии, слизано из официальной документации, с небольшими изменениями. Одно из главных — использование публичного ключа, для получения информации из токена.
./src/utils/securityMiddleware.js
Это промежуточная функция будет использоваться для защиты наших маршрутов
export const securityMiddleware = (req, res, next, passport, groups) => { passport.authenticate('jwt', { session: false }, (err, user) => { if (err) { return next(err); } if (!user) { return res.status(401).send('Unauthorized'); } // добавляем в req поле user с определенным набором полей, отдавать здесь хэш, соль не надо. const { _id, email, nickname, group } = user; req.user = { _id, email, nickname, group }; if (groups.includes(user.group)) { return next(); } return res.status(403).send('Insufficient access rights'); })(req, res, next); };
Описание API
Наш минимальный API опишет процесс регистрации, аутентификации и авторизации, а также тестовые маршруты для проверки работы разных групп пользователей и открытых разделов.
Этот файл будет использоваться валидатором запросов, генератором документации и генератором маршрутов.
Обратите внимание на поля operationId – это имена функций-контроллеров, которые мы реализуем и они будут вызываться, чтобы обработать эндпоинты.
./src/api/apiV1.yaml
openapi: 3.0.3 info: title: Passport test description: Test of passport.js version: 1.0.0 license: name: MIT License url: https://opensource.org/licenses/MIT paths: # Тестовый публичный маршрут /test/ping: get: description: 'Returns pong' tags: - Test operationId: ping responses: '200': description: OK $ref: '#/components/responses/standardResponse' # Тестовый маршрут, для зарегистрированных пользователей /test/private: get: description: 'Testing private section' tags: - Test operationId: testPrivate security: - access: ['free'] responses: '200': $ref: '#/components/responses/standardResponse' # Тестовый маршрут, для платных подписчиков /test/subscription: get: description: 'Testing subscribers section' tags: - Test operationId: testSubscription security: - access: ['subscriber'] responses: '200': $ref: '#/components/responses/standardResponse' # Маршрут для регистрации пользователей # обратите внимание на поле email, валидатор будет ожидать формат email /user/register: post: description: 'Register user' tags: - User operationId: userRegister requestBody: required: true content: application/json: schema: type: object properties: email: type: string format: email nickname: type: string password: type: string format: password responses: '200': description: OK $ref: '#/components/responses/standardResponse' '400': description: Bad Request $ref: '#/components/responses/standardResponse' # Маршрут для аутентификации /user/login: post: description: 'Login user' tags: - User operationId: userLogin requestBody: required: true content: application/json: schema: type: object properties: email: type: string format: email password: type: string format: password responses: '200': description: Returns boolean success state and jwt object content: application/json: schema: type: object properties: success: type: boolean jwt: type: object '400': description: Login failed $ref: '#/components/responses/standardResponse' /user/refresh: get: description: 'Refresh token' tags: - User operationId: userRefreshToken responses: '200': description: 'Token refreshed' $ref: '#/components/responses/jwtResponse' '403': description: 'Token refresh error' $ref: '#/components/responses/standardResponse' /user/logout: get: description: 'Logout user. Remove cookie. Remove refresh token in DB' tags: - User operationId: userLogout responses: '200': description: 'Successfully logged out' $ref: '#/components/responses/standardResponse' '403': description: 'You are not logged in to logout!' $ref: '#/components/responses/standardResponse' /user/profile: get: description: 'Returns user object' tags: - User operationId: userProfile security: - access: [ 'free' ] responses: '200': $ref: '#/components/responses/standardResponse' '403': $ref: '#/components/responses/standardResponse' components: responses: standardResponse: description: Returns boolean success state and string message content: application/json: schema: type: object properties: success: type: boolean message: type: string jwtResponse: description: Returns boolean success state and jwt object content: application/json: schema: type: object properties: success: type: boolean jwt: type: object
Модель пользователя и mongoose
Процесс создания базы данных и настройку доступа пользователя к ней, я описывать не буду, процесс максимально доступно описан в официальной документации.
В корне проекта создайте файл .env и укажите в нем порт, на котором будет работать ваше приложение, а также параметры подключения к БД.
.env
PORT = 3007 DB_HOST = localhost DB_PORT = 27017 DB_NAME = passport DB_USER = passport DB_PASS = passport
Настройка подключения к БД
./src/db/db.js
import Mongoose from 'mongoose'; export const connect = async () => { const dbHost = process.env.DB_HOST; const dbPort = process.env.DB_PORT; const dbName = process.env.DB_NAME; const user = process.env.DB_USER; const pass = process.env.DB_PASS; const uri = `mongodb://${dbHost}:${dbPort}/${dbName}?authSource=dbWithCredentials`; await Mongoose.connect(uri, { authSource: dbName, user, pass, useNewUrlParser: true, useFindAndModify: true, useUnifiedTopology: true, useCreateIndex: true }).catch((err) => console.error(err)); const db = Mongoose.connection; db.on('error', () => { throw new Error('Error connecting database'); }); };
Модель нашего пользователя
./src/db/models/User.js
import mongoose from 'mongoose'; export const userSchema = new mongoose.Schema( { email: { type: String, required: true, unique: true }, nickname: { type: String, required: true, unique: true }, hash: { type: String, required: true }, salt: { type: String, required: false }, refreshToken: { type: Object }, group: { type: String } }, { versionKey: false } );
Обработка эндпоинтов
Контроллеры имеет смысл группировать по функционалу в один файл, если там много всего, то возможно даже по отдельным директориям. В нашем случае хватит файлов.
Контроллер user.js содержит функции, логика работы которых подробно описана в диаграммах в разделе про безопасность. Здесь без особых комментариев, код должен быть вполне понятен.
./src/api/controllers/user.js
import mongoose from 'mongoose'; import { userSchema } from '../../db/models/User'; import * as cryptoHelper from '../../utils/cryptoHelper'; import * as jwtHelper from '../../utils/jwtHelper'; const User = mongoose.model('User', userSchema); async function sendAndSetTokens(req, res, user) { const jwt = jwtHelper.issueJWT(user); const refresh = jwtHelper.issueRefresh(user); user.refreshToken = refresh; await user.save(); res.cookie('refreshToken', refresh, { secure: true, httpOnly: true }); res.status(200).json({ success: true, jwt: { token: jwt.token, expiresIn: jwt.expires } }); } // удаление refresh токена из базы async function resetRefresh(user) { user.refreshToken = ''; await user.save(); } // Сброс куки, путем установки пустого значения и короткого срока жизни function resetCookie(req, res) { res.cookie('refreshToken', '', { maxAge: 1000, secure: true, httpOnly: true }); res.status(200).json({ success: true, message: 'Successfully logged out' }); } export function userRegister(req, res, next) { const { email, nickname, password } = req.body; const saltHash = cryptoHelper.genHashWithSalt(password); const { salt, hash } = saltHash; const newUser = new User({ email, nickname, hash, salt, group: 'free' }); newUser .save() .then(() => { res.status(200).json({ success: true, message: 'User registered' }); }) .catch((err) => { if (err.code === 11000 || err.code === 11001) { res.status(409).json({ success: false, message: `E-mail ${email} already registered, try another or log in.` }); } else { res.status(400).json({ success: false, message: err.message }); } }); } export function userLogin(req, res, next) { User.findOne({ email: req.body.email }) .then(async (user) => { if (!user) { res.status(401).json({ success: false, message: 'Wrong login or password' }); } const isValid = cryptoHelper.validatePassword(req.body.password, user.hash, user.salt); if (isValid) { await sendAndSetTokens(req, res, user); } else { res.status(401).json({ success: false, message: 'Wrong login or password' }); } }) .catch((err) => next(err)); } export function userRefreshToken(req, res, next) { const refreshCandidate = req.cookies.refreshToken; if (refreshCandidate) { if (jwtHelper.isValidRefresh(refreshCandidate.token)) { User.findOne({ refreshToken: refreshCandidate }) .then(async (user) => { await sendAndSetTokens(req, res, user); }) .catch(() => { res.status(403).json({ success: false, message: 'Invalid Refresh Token!' }); }); } else { res.status(403).json({ success: false, message: 'Invalid Refresh Token!' }); } } else { res.status(401).json({ success: false, message: 'Refresh Token Empty!' }); } } export function userLogout(req, res, next) { const refreshCandidate = req.cookies.refreshToken; if (refreshCandidate) { if (jwtHelper.isValidRefresh(refreshCandidate.token)) { User.findOne({ refreshToken: refreshCandidate }) .then(async (user) => { await resetRefresh(user); resetCookie(req, res); }) .catch((err) => next(err)); } else { res.status(401).json({ success: false, message: 'You are not logged in to logout!' }); } } else { res.status(401).json({ success: false, message: 'Refresh Token Empty!!' }); } } export function userProfile(req, res, next) { if (req.user) { res.status(200).json(req.user); } }
Контроллер test.js – набор простейших функций, для проверки работы авторизации и работы незащищенного маршрута.
./src/api/controllers/test.js
export function ping(req, res) { res.json({ success: true, message: 'Pong' }); } export function testSubscription(req, res) { res.status(200).json({ success: true, message: 'You are subscriber!' }); } export function testPrivate(req, res) { res.status(200).json({ success: true, message: 'You are in Private!' }); }
Осталось экспортировать все это разом в ./src/api/controllers/index.js
export * from './test'; export * from './user';
Собираем все воедино
Нам осталось собрать все в кучу и написать точку входа. Для этого мы напишем server.js и положим его в ./src/utils и app.js, который положим в ./src
На этих файлах остановимся подробнее. Начнем с импортов, что для чего нужно:
-
express – сам наш сервер
-
cookieParser – промежуточное ПО, которое позволит нам работать с куки
-
swaggerUI – интерфейс документации, который строится на основании описания API в yaml файле.
-
swagger-routes-express – автоматическая генерация маршрутов (линковка эндпоинтов к функциям контроллеров на основании того же yaml файла API)
-
yaml – работа с yaml файлами
-
express-openapi-validator – простой валидатор запросов (может и ответы валидировать, но я не включал. Включается элементарно изменением значения в true)
-
morgan – мощный инструмент логирования, который я использую для вывода информации в консоль, чтобы дебажить в реальном времени.
-
cors – установка заголовков CORS, чтобы не делать ручками. Немного подробнее поговорим ниже.
-
passport – та самая библиотека, которая упрощает нам работу по защите маршрутов
-
дальше подключаем контроллеры, базу, стратегию passport.
Теперь первым делом инициализируем нашу стратегию, передав ей объект passport:
strategy(passport);
Подключаемся к БД:
db.connect() .then(() => console.log('MongoDB connected')) .catch((error) => console.error(error));
Загружаем и выводим в консоль информацию по API:
const yamlSpecFile = './bin/api/apiV1.yaml'; const apiDefinition = YAML.load(yamlSpecFile); const apiSummary = summarise(apiDefinition); console.info(apiSummary);
Инициализируем инстанс express:
const server = express();
Настройка сервера
// подключаем логирование с помощью morgan server.use(morgan('dev')); // позволяем себе читать параметры из url server.use(express.urlencoded({ extended: true })); // это промежуточное по позволяет парсить входящие запросы с application/json server.use(express.json()); // позволяет работать с куки server.use(cookieParser()); // настройка CORS. В боевом проекте стоит указать адреса, для которых будет доступен наш БЭК //var corsOptions = { // origin: 'http://example.com', // optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 //} // cors(corsOptions) // Более подробно смотрите документацию пакета на npmjs.com server.use(cors()); // инициализируем passport.js server.use(passport.initialize());
Автоматическая валидация запросов
// Чтобы включить валидацию ответов, поправьте параметр validateResponses // обратите внимание, что здесь мы указываем yaml файл API const validatorOptions = { coerceTypes: false, apiSpec: yamlSpecFile, validateRequests: true, validateResponses: false }; server.use(OpenApiValidator.middleware(validatorOptions)); // Кастомизация ошибок, если валидация не пройдена server.use((err, req, res, next) => { res.status(err.status).json({ error: { type: 'request_validation', message: err.message, errors: err.errors } }); });
Самый главный участок – генерация маршрутов и их защита.
Коннектору передается объект, в который мы импортировали все функции контроллеров и описание API. На основании этих данных он линкует и создает маршруты, которые в стандартной документации и гайдах выглядят как
server.use('/route/to/something', controllerFunction...
у нас этого не будет.
Также обратите внимание на объект security, объекты subscriber и free, это поля из yaml файла описания api, в разделе security acess. Промежуточному ПО здесь мы передаем стандартный набор для middleware + объект paspport + массив групп, которым разрешен доступ к маршрутам, отмеченным определенным уровнем доступа.
const connect = connector(api, apiDefinition, { onCreateRoute: (method, descriptor) => { console.log( `Method ${method} of endpoint ${descriptor[0]} linked to ${descriptor[1].name}` ); }, security: { subscriber: (req, res, next) => { securityMiddleware(req, res, next, passport, ['subscriber', 'admin']); }, free: (req, res, next) => { securityMiddleware(req, res, next, passport, ['free', 'subscriber', 'admin']); } } });
Осталось обернуть наш сервер коннектором и экспортировать
connect(server); module.exports = server;
./src/utils/server.js
import express from 'express'; import cookieParser from 'cookie-parser'; import swaggerUi from 'swagger-ui-express'; import { connector, summarise } from 'swagger-routes-express'; import YAML from 'yamljs'; import * as OpenApiValidator from 'express-openapi-validator'; import morgan from 'morgan'; import cors from 'cors'; import passport from 'passport'; import * as api from '../api/controllers'; import * as db from '../db/db'; import { securityMiddleware } from './securityMiddleware'; import { strategy } from './passport'; strategy(passport); // connect to DB db.connect() .then(() => console.log('MongoDB connected')) .catch((error) => console.error(error)); // load API definition const yamlSpecFile = './bin/api/apiV1.yaml'; const apiDefinition = YAML.load(yamlSpecFile); const apiSummary = summarise(apiDefinition); console.info(apiSummary); const server = express(); server.use(morgan('dev')); server.use(express.urlencoded({ extended: true })); server.use(express.json()); server.use(cookieParser()); server.use(cors()); server.use(passport.initialize()); // API Documentation server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDefinition, { explorer: false })); // Automatic validation const validatorOptions = { coerceTypes: false, apiSpec: yamlSpecFile, validateRequests: true, validateResponses: false }; server.use(OpenApiValidator.middleware(validatorOptions)); // error customization, if request is invalid server.use((err, req, res, next) => { res.status(err.status).json({ error: { type: 'request_validation', message: err.message, errors: err.errors } }); }); // Automatic routing based on api definition const connect = connector(api, apiDefinition, { onCreateRoute: (method, descriptor) => { console.log( `Method ${method} of endpoint ${descriptor[0]} linked to ${descriptor[1].name}` ); }, security: { subscriber: (req, res, next) => { securityMiddleware(req, res, next, passport, ['subscriber', 'admin']); }, free: (req, res, next) => { securityMiddleware(req, res, next, passport, ['free', 'subscriber', 'admin']); } } }); connect(server); module.exports = server;
Осталась точка входа – app.js. Здесь все достаточно просто, распишу все в комментариях.
import https from 'https'; import fs from 'fs'; import * as dotenv from 'dotenv'; import server from './utils/server'; // помещаем в process.env переменные из .env файла dotenv.config(); const { PORT } = process.env; // загружаем сертификат и закрытый ключ const privateKey = fs.readFileSync('./bin/crypto/ssl.key'); const certificate = fs.readFileSync('./bin/crypto/ssl.crt'); const options = { key: privateKey, cert: certificate }; // создаем HTTPS сервер const app = https.createServer(options, server); // запускаем на порту, который указали в .env файле app.listen(PORT, () => { console.info(`Listening on https://localhost:${PORT}`); console.info(`Open https://localhost:${PORT}/api-docs for documentation`); });
Основная информация взята из этих статей:
https://losikov.medium.com/part-2-express-open-api-3-0-634385c97a4e
Спасибо за внимание, надеюсь кому-то этот лонгрид поможет .
ссылка на оригинал статьи https://habr.com/ru/post/559136/
Добавить комментарий