Нейронные сети на Javascript
Идея для написания этой статьи возникла прошлым летом, когда я слушал доклад на конференции BigData по нейронным сетям. Лектор «посыпал» слушателей непривычными словечками «нейрон», «обучающая выборка», «тренировать модель»… «Ничего не понял — пора в менеджеры», — подумал я. Но недавно тема нейронных сетей все же коснулась моей работы и я решил на простом примере показать, как использовать этот инструмент на языке JavaScript.
Мы создадим нейронную сеть, с помощью которой будем распознавать ручное написание цифры от 0 до 9. Рабочий пример займет несколько строк. Код будет понятен даже тем программистам, которые не имели дело с нейронными сетями ранее. Как это все работает, можно будет посмотреть прямо в браузере.
Если вы уже знаете что такое Perceptron, следующую главу нужно пропустить.
Совсем немного теории
Нейронные сети возникли из исследований в области искусственного интеллекта, а именно, из попыток воспроизвести способность биологических нервных систем обучаться и исправлять ошибки, моделируя низкоуровневую структуру мозга. В простейшем случае она состоит из нескольких соединенных между собой нейронов.
Математический нейрон
Несложный автомат, преобразующий входные сигналы в результирующий выходной сигнал.
Сигналы x1, x2, x3 … xn, поступая на вход, преобразуются линейным образом, т.е. к телу нейрона поступают силы: w1x1, w2x2, w3x3 … wnxn, где wi – веса соответствующих сигналов. Нейрон суммирует эти сигналы, затем применяет к сумме некоторую функцию f(x) и выдаёт полученный выходной сигнал y.
В качестве функции f(x) чаще всего используется сигмоидная или пороговая функции.
Пороговая функция может принимать только два дискретных значения 0 или 1. Смена значения функции происходит при переходе через заданный порог T.+
Сигмоидная – непрерывная функция, может принимать бесконечно много значений в диапазоне от 0 до 1.
UPD: В комментариях также упоминаются функции ReLU и MaxOut как более современные.
Архитектура нейронной сети может быть разной, мы рассмотрим одну из простых реализаций нейронной сети — Perceptron
Архитектура Perceptron

Есть слой входных нейронов (где информация поступает из вне), слой выходных нейронов (откуда можно взять результат) и ряд, так-называемых, скрытых слоев между ними. Нейроны могут быть расположены в несколько слоёв. Каждая связь между нейронами имеет свой вес Wij
Входные и выходные сигналы
Перед тем, как подавать сигналы на нейроны входящего слоя сети нам их нужно нормализовать. Нормализация входных данных — это процесс, при котором все входные данные проходят процесс «выравнивания», т.е. приведения к интервалу [0,1] или [-1,1]. Если не провести нормализацию, то входные данные будут оказывать дополнительное влияние на нейрон, что приведет к неверным решениям. Другими словами, как можно сравнивать величины разных порядков?
На нейронах выходного слоя у нас тоже не будет чистой «1» или «0», это нормально. Есть некий порог, при котором мы будем считать, что получили «1» или «0». Про интерпретацию результатов поговорим позже.
«Пример в студию, а то уже засыпаю»
Для удобства я рекомендую себе поставить nodejs и npm.
Мы будем описывать сеть с помощью библиотеки Brain.js. В конце статьи я также дам ссылки на другие библиотеки, которые можно будет сконфигурировать похожим образом. Brain.js мне понравился своей скоростью и возможностью сохранять натренированную модель.
Давайте попробуем пример из документации — эмулятор функции XOR:
var brain = require('brain.js'); var net = new brain.NeuralNetwork(); net.train([, , , ]); var output = net.run([1, 0]); // [0.987] console.log(output);
запишем все в файл simple1.js, чтоб пример заработал, поставим модуль brain и запустим
npm install brain.js node simple1.js # [ 0.9331839217737243 ]
У нас 2 входящих нейрона и один нейрон на выходе, библиотека brain.js сама сконфигурирует скрытый слой и установит там столько нейронов, сколько сочтет нужным (в этом примере 3 нейрона).
То, что мы передали в метод .train называется обучающей выборкой, каждый элемент которой состоит из массива объектов со свойством input и output (массив входящих и выходящих параметров). Мы не проводили нормализацию входящих данных, так как сами данные уже приведены в нужную форму.
Обратите внимание: мы на выходе получаем не [0.987] а [0.9331. ]. У вас может быть немного другое значение. Это нормально, так как алгоритм обучения использует случайные числа при подборе весовых коэффициентов.
Метод .run применяется для получения ответа нейронной сети на заданный в аргументе массив входящих сигналов.
Другие простые примеры можно посмотреть в документации brain
Распознаем цифры

В начале нам нужно получить изображения с рукописными цифрами, приведенными к одному размеру. В нашем примере мы будем использовать модуль MNIST digits, набор тысяч 28x28px бинарных изображений рукописных цифр от 0 до 9:
Оригинальная база данных MNIST содержит 60 000 примеров для обучения и 10 000 примеров для тестирования, ее можно можно загрузить с сайта LeCun. Автор MNIST digits сделал доступной часть этих примеров для языка JavaScript, в библиотеке уже проведена нормализация входящих сигналов. С помощью этого модуля мы можем получать обучающую и тестовую выборку автоматически.
Мне пришлось клонировать библиотеку MNIST digits, так как там есть небольшая путаница с данными. Я повторно загрузил 10 000 примеров из оригинальной базы данных, так что использовать надо MNIST digits из моего репозитория.
Конфигурация сети
Во входном слое нам необходимо 28×28=784 нейрона, на выходе 10 нейронов. Скрытый слой brain.js сконфигурирует сам. Забегая наперед, уточню: там будет 392 нейрона. Обучающая выборка будет сформирована модулем mnist
Тренируем модель
Установим mnist
npm install https://github.com/ApelSYN/mnist
Все готово, обучаем сеть
const brain = require('brain.js'); var net = new brain.NeuralNetwork(); const fs = require('fs'); const mnist = require('mnist'); const set = mnist.set(1000, 0); const trainingSet = set.training; net.train(trainingSet, < errorThresh: 0.005, // error threshold to reach iterations: 20000, // maximum training iterations log: true, // console.log() progress periodically logPeriod: 1, // number of iterations between logging learningRate: 0.3 // learning rate >); let wstream = fs.createWriteStream('./data/mnistTrain.json'); wstream.write(JSON.stringify(net.toJSON(),null,2)); wstream.end(); console.log('MNIST dataset with Brain.js train done.')
Создаем сеть, получаем 1000 элементов обучающей выборки, вызываем метод .train, который производит обучение сети — сохраняем все в файл ‘./data/mnistTrain.json’ (не забудьте создать папку «./data»).
Если все сделали правильно, получите приблизительно такой результат:
[root@HomeWebServer nn]# node train.js iterations: 0 training error: 0.060402555338691676 iterations: 1 training error: 0.02802436102035996 iterations: 2 training error: 0.020358600820106914 iterations: 3 training error: 0.0159533285799183 iterations: 4 training error: 0.012557029942873513 iterations: 5 training error: 0.010245175822114688 iterations: 6 training error: 0.008218147206099617 iterations: 7 training error: 0.006798613211310184 iterations: 8 training error: 0.005629051609641436 iterations: 9 training error: 0.004910207736789503 MNIST dataset with Brain.js train done.
Все можно распознавать
Осталось написать совсем немного кода — и система распознавания готова!
const brain = require('brain.js'), mnist = require('mnist'); var net = new brain.NeuralNetwork(); const set = mnist.set(0, 1); const testSet = set.test; net.fromJSON(require('./data/mnistTrain')); var output = net.run(testSet[0].input); console.log(testSet[0].output); console.log(output);
Получаем 1 случайный тестовый пример из выборки 10 000 записей, загружаем натренированную ранее модель, передаем на вход сети тестовую запись и смотрим правильно ли она распозналась.
Вот пример выполнения
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 ] [ 0.0002863506627761867, 0.00002389940760904011, 0.00039954062883041345, 0.9910109896013567, 7.562879202664903e-7, 0.0038756598319246837, 0.000016752919557362786, 0.0007205981595354964, 0.13699517762991756, 0.0011053963693377692 ]
В примере в сеть на входящие нейроны поступила оцифрованная тройка (первый масив это идеальный ответ), на выходе сети мы получили массив елементов, один из которых близок к единице (0.9910109896013567) это тоже третий бит. Обратите внимание на четвертый бит там 7.56… в -7 степени, это такая форма записи чисел с плавающей точкой в JavaScript.
Ну что же, распознавание прошло правильно. Поздравляю, наша сеть заработала!
Немного «причешем» наши результаты функцией softmax, которую я взял из одного примера по машинному обучению:
function softmax(output) < var maximum = output.reduce(function(p,c) < return p>c ? p : c; >); var nominators = output.map(function(e) < return Math.exp(e - maximum); >); var denominator = nominators.reduce(function (p, c) < return p + c; >); var softmax = nominators.map(function(e) < return e / denominator; >); var maxIndex = 0; softmax.reduce(function(p,c,i) else return p;>); var result = []; for (var i=0; i return result; >
Функцию можно поместить в начало нашего кода и последнюю строку заменить на
console.log(softmax(output));
Все друзья — теперь все работает красиво:
[root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ] [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ] [root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ] [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ] [root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ] [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ]
Иногда сеть может давать неправильный результат (мы взяли небольшую выборку и поставили не достаточно строгую погрешность).
А как распознать цифру, которую напишете вы?
Конечно, тут нет никакой подтасовки, но все же хочется самому проверить «на прочность» то, что получилось.
С помощью HTML5 Canvas и все тем же brain.js-ом с сохраненной моделью мне удалось сделать реализацию распознавания в браузере, часть кода для отрисовки и дизайн интерфейса я позаимствовал в интернете. Можете попробовать вживую. В мобильном устройстве рисовать можно пальцем.
Ссылки по теме
- Библиотеки на JavaScript для работы с нейронными сетями
- brain.js
- Synaptic.js
- ConvNetJS
- Mind.js
UPD: Альтернативные реализации живого примера 1, 2 на JavaScript из комментариев и личной переписки.
Node.JS *
Как сгенерировать безопасный, типизированный Node.js API с использованием Prisma, TypeGraphQL и graphql-query-purifier
Уровень сложности Средний
Время на прочтение 3 мин
Количество просмотров 2.5K
Создание эффективного Node.js API, обеспечивающего безопасность данных и типобезопасность, может быть сложной задачей. Это руководство демонстрирует процесс с использованием Prisma, TypeGraphQL и graphql-query-purifier.
Всего голосов 9: ↑1 и ↓8 -7
Добавить в закладки 7Новости
все подряд лучшие
DmitryKazakov8 11 ноя в 16:40Webpack vs esbuild — уже можно использовать в production?
Уровень сложности Средний
Время на прочтение 12 мин
Количество просмотров 6.7K
Периодически я пробую разные инструменты, и если они стабильно покрывают все необходимые сценарии — включаю в свою экосистему для коммерческих проектов. С третьего подхода за последние 3 года esbuild, наконец, приблизился по функционалу к Webpack. В статье привожу проблемы, с которыми я столкнулся при миграции, и пути их решения.
Всего голосов 22: ↑22 и ↓0 +22
Добавить в закладки 60
kmoseenk 7 ноя в 13:42Какой монорепозиторий сейчас подходит для Node.js BACKEND?
Время на прочтение 13 мин
Количество просмотров 4.6K
На рынке монорепозиториев сейчас жара. Расцвет новых инструментов приводит многих в замешательство — что выбрать для следующего проекта? На что обратить внимание при выборе инструмента монорепо? В этой заметке мы постараемся разобраться в этом информационном перегрузе: поговорим о новых инструментах, подчеркнем важное и поделимся рекомендациями.
Всего голосов 13: ↑12 и ↓1 +11
Добавить в закладки 24
pomazkovjs 30 окт в 16:59Как подготовиться к собеседованию с помощью Telegram-бота на Node.js
Уровень сложности Простой
Время на прочтение 14 мин
Количество просмотров 6K
В этой статье мы разработаем бота для подготовки к собеседованиям. Он будет задавать вопросы по HTML, CSS, JS и React. При этом часть из них будет с вариантами ответа, а часть — без. Базу вопросов вы сможете пополнять самостоятельно.
Во время разработки вы научитесь работать с Telegram Bot API с помощью grammY и Node.js, а также самостоятельно деплоить ботов на сервер.
Всего голосов 39: ↑37 и ↓2 +35
Добавить в закладки 75Истории
sahsAGU 30 окт в 07:32
Создание приложения для real-time обмена геоданными с React, Socket.io и Leaflet
Уровень сложности Средний
Время на прочтение 14 мин
Количество просмотров 3.8K
Есть много руководств о том, как сделать приложение для общения в реальном времени на React и Socket.io. Создание таких приложений в том числе полезно для самообучения. Но мне захотелось чего-то более творческого. Пришла идея сделать приложение, где можно делиться местоположением.
Всего голосов 7: ↑6 и ↓1 +5
Добавить в закладки 61
don_alex_88 25 окт в 21:16Простой пример ИИ для управления роботом. TensorFlow + Node Js
Уровень сложности Простой
Время на прочтение 6 мин
Количество просмотров 6.3K
Немного слов обо мне: мое хобби это робототехника. На данный момент экспериментирую с шагающим роботом на базе SunFounder PiCrawler.
Последнее время тема искусственного интеллекта (ИИ) приобретает все большую популярность. Причиной этому служит в том числе совершенствование мобильных устройств и компьютеров — они становятся мощнее и компактнее.
В данной статье я постараюсь простыми словами объяснить, как можно применить ИИ для управления роботом, используя готовую библиотеку TensorFlow.
Всего голосов 7: ↑7 и ↓0 +7
Добавить в закладки 77
arsis-ltd 21 окт в 12:30Разработка RESTful API: как использовать ExpressJS и другие фреймворки для создания RESTful API
Время на прочтение 9 мин
Количество просмотров 3K
RESTful API — это одна из самых популярных архитектурных моделей для создания веб-сервисов и приложений. Понимание основных концепций REST и принципов RESTful API играет решающую роль при разработке современных веб-приложений.
Всего голосов 9: ↑7 и ↓2 +5
Добавить в закладки 30
AlexMeshock 20 окт в 10:21Как я создавал мобильное приложение для хранения прочитанных книг BookDesk
Уровень сложности Простой
Время на прочтение 9 мин
Количество просмотров 3.6K
Всем привет! Меня зовут Александр, мне 33 года и я React-разработчик из Беларуси (10 лет опыта во фронтенде).
Я хочу рассказать о моем опыте создания мобильного приложения для хранениях прочитанных книг BookDesk.
Все началось еще в 2020 году, когда началась пандемия коронавируса, и всем пришлось находиться в изоляции. Тогда и пришла идея о создании своего приложения. У меня богатый опыт разработки веб-приложений на React, и поэтому я решил попробовать себя в новом направлении. Я решил долго не думать над технологиями и использовать React Native в связке с Node.js и MongoDB, т. к. ранее я уже работал с Node.js и MongoDB для создания веб приложений и, поэтому, выбор был очевиден.
Всего голосов 14: ↑14 и ↓0 +14
Добавить в закладки 46
Infinity_kim 14 окт в 20:29ESLint | Shareable config настройка и что это такое?
Уровень сложности Средний
Время на прочтение 5 мин
Количество просмотров 3.4K
Приветствую! В ходе своей профессиональной деятельности, каждый фронтенд-разработчик сталкивается с неотъемлемой частью разработки — инструментом ESLint.
Сегодня мы поговорим, о настройке shareable config. Мы рассмотрим, как это может помочь продуктовым командам или фрилансеру разработчику, а далее перейдем к настройке этой конфигурации.
Всего голосов 13: ↑10 и ↓3 +7
Добавить в закладки 36
KOKNIWEX 14 окт в 13:01Как я использовал тематическое разделение доступа для создания веб-приложения
Уровень сложности Простой
Время на прочтение 5 мин
Количество просмотров 1.8K
В этой статье я хочу поделиться своим опытом использования тематического разделения доступа (ТРД) для создания веб-приложения, которое позволяет пользователям обмениваться сообщениями по разным темам. ТРД — это метод управления доступом, при котором права доступа субъектов системы на объекты группируются с учётом специфики их применения, образуя роли¹. Например, в моём приложении есть роли администратора, модератора, автора и читателя. Каждая роль имеет свой набор прав на разные действия, такие как создание, редактирование, удаление и просмотр сообщений.
Всего голосов 8: ↑8 и ↓0 +8
Добавить в закладки 19
ovsale 13 окт в 14:37Классификация комментариев к Youtube видео с помощью GPT моделей. Точность и ее цена
Уровень сложности Простой
Время на прочтение 7 мин
Количество просмотров 2.1K
Добрый день
В этой статье я бы хотел поделиться моим опытом классификации комментариев к Youtube видео при помощи OpenAI моделей gpt-3.5 и gpt-4.Всего голосов 4: ↑4 и ↓0 +4
Добавить в закладки 18
LyuMih 8 окт в 15:57Hyper: Chance. Этап 0. Знакомство
Уровень сложности Простой
Время на прочтение 3 мин
Количество просмотров 823
Hyper Chance: Этап 0. Знакомство
Hyper Chance — бесплатный технический акселератор коммерческих проектов от Hyper Dev Guild для разработчиков на основе фреймворка $mol
Обратная связь приветствуется! ^-^
Всего голосов 3: ↑2 и ↓1 +1
Добавить в закладки 7
Cloud4Y 6 окт в 10:59Дорогая, я уменьшил пакет npm
Уровень сложности Средний
Время на прочтение 8 мин
Количество просмотров 3.1K
При разработке программного обеспечения исходный код почти всегда поставляется в виде файлов .tar.gz или .tgz. Но gzip начинает устаревать, уступая место более новым, более современным алгоритмам сжатия, таким как Brotli и ZStandard. Теперь представьте себе мир, в котором npm использует один из этих новых алгоритмов. В статье поговорим о сжатии и возможности модернизации стратегии сжатия npm.
Всего голосов 8: ↑8 и ↓0 +8
Добавить в закладки 11Ближайшие события

Дата 25 сентября – 30 ноября
Место Онлайн
Дата 13 – 26 ноября
Время 18:00 – 23:00
Место Москва • Нижний Новгород • Екатеринбург • Новосибирск • Санкт-Петербург • Онлайн
Дата 22 – 23 ноября
Время 10:00 – 20:00
Место Москва
Дата 22 ноября
Время 14:00 – 19:00
Место Онлайн
Дата 22 – 24 ноября
Время 10:00 – 19:30
Место Москва • Онлайн
Дата 27 – 28 ноября
Время 9:00 – 20:00
Место Москва • Онлайн
Дата 27 – 28 ноября
Время 10:00 – 20:00
Место Онлайн
Дата 28 ноября
Время 10:00 – 17:00
Место Москва • Онлайн
Дата 30 ноября
Время 17:30 – 00:00
Место Москва
Дата 1 декабря
Время 12:30
Место Москва • Онлайн
Дата 5 – 6 декабря
Время 9:00 – 23:59
Место Москва • Белград • Онлайн
Дата 9 – 11 декабря
Время 10:00 – 17:00
Место Москва
Дата 18 – 20 декабря
Время 10:00 – 17:00
Место Москва
Дата 19 – 20 января
Время 10:00 – 18:00
Место Москва
Влево Вправо
alxgrn 3 окт в 15:05Обновление состояния системы через Server-Sent Events (SSE) без затей
Уровень сложности Средний
Время на прочтение 11 мин
Количество просмотров 3K
В любом многопользовательском проекте рано или поздно возникает потребность в оперативном уведомлении клиентов об изменении объектов в системе. В данной статье я не буду рассматривать что-то сложное, типа совместного редактирования документов, а напишу о более простой ситуации, которая встречается повсеместно.
Допустим, у нас есть список объектов, к примеру – список эпизодов для шоу. Один клиент на него смотрит, а другой в это время добавляет в список еще один эпизод. Хорошо бы факт добавления нового эпизода сразу отобразить у первого клиента. То же самое относится и к просмотру информации об отдельном эпизоде: если один клиент её просматривает, а другой – редактирует, было бы здорово результат редактирования сразу отображать у первого.
Для решения данной задачи очень удобно использовать механизм Server-Sent Events (SSE). О том как это сделать в проектах, у которых на беке Node, а на фронте React, я и хочу поговорить.
Обзор nest.js: как эффективно его использовать
Добрый день. В нашей компании мы очень трепетно относимся к архитектуре наших решений и удобству ее использования. Мы стараемся убирать узкие места для получения лучшей производительности, а также для сокращения времени разработки новых фич. Одним из таких способов мы решили поделиться с вами.

Сегодня поговорим о фронт-оптимизации, об express, nest.js и о том, как этим правильно пользоваться. Думаю, все уже обратили внимание на тенденцию, которой следуют фронтовые разработчики: делать бек на node.js. Это значит, что для фронта настало золотое время – давайте разберёмся почему. Сразу отмечу, что я нарочно буду избегать разбора механизмов работы node.js, а также всех его плюсов и минусов, так как сегодня речь пойдет не об этом.
Зачем писать бек на node.js?
Исходя из своего опыта, могу сказать, что эта программная платформа применяется в случаях, когда требуется вынести часть бизнес-логики в прослойку между историческим беком и свежим фронтом. Я встречал ситуации, где node.js служил инструментом для перехода от одного типа архитектуры (монолит) к другому (микросервисной). В качестве промежуточного вывода можем заключить: node.js позволяет нам создавать унифицированное API, если по какой либо причине бек не может выполнить свою работу.
На чистом node.js практически никто не пишет: большинство, как правило, используют какой-либо фреймворк. Давайте рассмотрим один из самых популярных на данный момент таких фреймворков.
Express
Большинство приложений написано на простом и минималистичном фреймворке express.js. Он прост в освоении, гибок и даже вроде как позволяет всё сделать легко. Однако у него есть недостатки, о которых расскажу далее.
Первый и самый главный — гибкость. Это означает, что при отсутствии навыков его использования вы или совершите ошибки, или случайно откроете временной портал.
Второй недостаток — импорт. Данный функционал реализован не слишком нативно, поэтому если вы просчитались с архитектурой, то мешанины вам не избежать.

Третий недостаток состоит в том, что большинство проверок на тип и пустоту требуется осуществлять вручную, так как в express.js применяется ES, а не TS. Также к минусам отнесу пляски с выставлением кукис и прочим серверным взаимодействием со стороны UI.
Все вышеперечисленное свидетельствует о том, что в express.js используется устаревший паттерн callback-функций, что в свою очередь зачастую приводит к печальным последствиям. Ещё раз напомню, что использование express.js в чистом виде практически невозможно, за исключением каких-то простых приложений. Поэтому я предлагаю взамен ему использовать nest.js.
Nest.js
Nest.js — фреймворк для создания серверных приложений на node.js, расскажу о его особенностях, плюсах и минусах.
Nest.js написан на TypeScript и полностью поддерживает его (даже современные версии). Что это нам дает?
- Проверку на тип данных: как простых, так и сложных. Enum и прочие опции сильно выручают.
- Декораторы – функционал декларативного программирования, позволяющий расширить любые методы, какие только пожелаем.
- Классические импорты TypeScript (до боли знакомые разработчикам Angular). Побочным эффектом послужит ускорение разработки, а также возможность писать код большой командой, не мешая друг другу.
- Четкое разделение функциональной нагрузки по элементам:
- Interceptor — отвечает за дополнение, перехват запросов.
- Guards — отвечает за проверки доступности по каким-либо критериям.
- Pipes — выполняет две роли. Первая — это преобразование одного типа в другой. Например, когда у вас ID ожидается строкой, а пришло число, и чтобы не страдать преобразованием типов на фронте, pipe реализует это на беке. Вторая функция — это проверка соответствия типов.
- Custom route decorators — если вы считаете себя гениями, которым море по колено, то можете создать что-то жизненно необходимое.
- Exception filters — это все исключения, которые могут быть (согласно сетевому взаимодействию). В тех случаях, когда мы забыли сделать кастомную обработку ошибок, мы все равно получим данные по ним.
- Middleware — предобработка запросов с доступами ко всей информации запроса.
- Modules — используется для организации кода и архитектуры приложения.
- Controllers — отвечает за обработку входящих запросов и возврат ответов клиенту. А также за роутинг, парсинг параметров, в общем за всё, что касается принятия и отдачи по запросу.
- Nest.js поддерживает любые протоколы в добавок к HTTP, например, на основе RabbitMQ, Nats, Kafka или даже просто TCP-протокола. Все это доступно просто из коробки, без плясок с бубном, до установки модулей, плагинов, расширений и прочего. А еще присутствует CLI — это значит, что с помощью командной строки код у нас будет генерироваться, как нужно, и прописываться везде, где нужно, что, в свою очередь, значительно ускорит работу. Не правда ли, звучит знакомо? Особенно для тех, кто хоть когда-то писал на Angular. Теперь о плюсах и минусах. Плюсы:
- Легко использовать, учиться и осваивать.
- Мощный интерфейс командной строки для повышения производительности и упрощения разработки.
- Подробная и отлаженная документация с примерами (можно прочесть на https://nestjs.com/, а еще у них есть платные курсы, первый раз такое вижу).
- Открытый исходный код.
- Простые приложения для модульного тестирования.
- Создан для монолитов и микросервисов. Минусы:
- Требуется выучить большой объём информации, чтобы правильно все использовать.
- Придётся много писать DTO, для того чтобы данные корректно обрабатывались в запросе.
- Придётся иногда делать множественные проверки по типам, например, Enum и прочие. Резюмируя: Nest.js — прекрасный инструмент, который позволяет делать много, быстро, а главное, качественно, в обмен на хорошее описание DTO и прочих объектов. По мне, это небольшая плата за то, что снимает с меня львиную долю головной боли по проверкам, ответам, дополнению запросов и прочей рутины. И вот вам еще хинт, как из минусов выжать максимальное количество плюсов 😉
Как правильно пользоваться Nest.js

- Axios — потребуется для перехвата и подсовывания в него мок (для тестов самое то).
- @apidevtools/swagger-parser — позволяет автогенерировать код со swagger бека для упрощения написания интерфейсов, что значительно ускоряет написание кода.
- Swagger-ui-express — позволяет создать свой swagger и тестировать свою документацию (можно отдельно отдавать на тестирование).
- Swagger-typescript-api — позволяет спарсенный код swagger преобразовать в TypeScript и использовать для нужд проекта.
А еще пригодится https://editor.swagger.io/, в него можно вставить то, что сгенерировал наш swagger. Если кликнуть на Generate Client, а затем на typescript-angular, мы получим архив с автосгенерированными сервисами и интерфейсами для нашего приложения. Этот же архив можно использовать для react на тайп-скрипте. Если вы его внимательно изучите, то обнаружите, что он много для чего может подойти, чем облегчит вам жизнь и ускорит разработку.
P.S. Таков мой опыт использования фреймворков node.js, надеюсь, он окажется для вас полезным. С радостью отвечу на все ваши вопросы.
Автор: Дмитрий Ивко, ведущий разработчик Центра продуктов Dozor компании «РТК-Солар»
Тестирование RESTful API на NodeJS с Mocha и Chai

Перевод руководства Samuele Zaza. Текст оригинальной статьи можно найти здесь.
Я до сих пор помню восторг от возможности наконец-то писать бекэнд большого проекта на node и я уверен, что многие разделяют мои чувства.
А что дальше? Мы должны быть уверены, что наше приложение ведет себя так, как мы того ожидаем. Один из самых распространенных способов достичь этого — тесты. Тестирование — это безумно полезная вещь, когда мы добавляем новую фичу в приложение: наличие уже установленного и настроенного тестового окружения, которое может быть запущено одной командой, помогает понять, в каком месте новая фига породит новые баги.
Ранее мы обсуждали разработку RESTful Node API и аутентификацию Node API. В этом руководстве мы напишем простой RESTful API и используем Mocha и Chai для его тестирования. Мы будем тестировать CRUD для приложения «книгохранилище».Как всегда, вы можете все делать по шагам, читая руководство, или скачать исходный код на github.
Mocha: Тестовое окружение
Mocha — это javascript фреймворк для Node.js, который позволяет проводить асинхронное тестирование. Скажем так: он создает окружение, в котором мы можем использовать свои любимые assert библиотеки.

Mocha поставляется с огромным количеством возможностей. На сайте их огромный список. Больше всего мне нравится следующее:
- простая поддержка асинхронности, включая Promise
- поддержка таймаутов асинхронного выполнения
- before , after , before each , after each хуки (очень полезно для очистки окружения перед тестами)
- использование любой assertion библиотеки, которую вы заходите (в нашем случае Chai)
Chai: assertion библиотека
Итак, с Mocha у нас появилось окружение для выполнения наших тестов, но как мы будем тестировать HTTP запросы, например? Более того, как проверить, что GET запрос вернул ожидаемый JSON в ответ, в зависимости от переданных параметров? Нам нужна assertion библиотека, потому что mocha явно недостаточно.
Для этого руководства я выбрал Chai:

Chai дает нам своду выбора интерфейса: «should», «expect», «assert». Лично использую should, но вы можете выбрать любую. К тому же у Chai есть плагин Chai HTTP, который позволяет без затруднений тестировать HTTP запросы.
PREREQUISITES
- Node.js: базовое понимание node.js и рекомендуется базовое понимание RESTful API (я не буду сильно углубляться в детали реализации).
- POSTMAN для выполнения запросов к API.
- Синтакс ES6: я решил использовать последнюю версию Node (6..), в которой хорошо реализована интеграция ES6 features для лучшей читаемости кода. Если вы еще не очень дружите с ES6, вы можете почитать отличные статьи (Pt.1, Pt.2 and Pt.3). Но не беспокойтесь, я буду давать пояснения когда встретится какой-нибудь особенные синтаксис.
Настало время настроить наше книгохранилище.
Настройка проекта
Структура папок
Структура проекта будет иметь следующий вид:
-- controllers ---- models ------ book.js ---- routes ------ book.js -- config ---- default.json ---- dev.json ---- test.json -- test ---- book.js package.json server.jsonОбратите внимание, что папка /config содержит 3 JSON файла: как видно из названия, они содержат настройки для различного окружения.
В этом руководстве мы будем переключаться между двумя базами данных — одна для разработки, другая для тестирования. Такие образом, файлы будут содержать mongodb URI в JSON формате:
dev.json и default.json :
Больше о файлах конфигурации (папка config, порядок файлов, формат файлов) можно почитать [тут] (https://github.com/lorenwest/node-config/wiki/Configuration-Files).
Обратите внимание на файл /test/book.js , в котором будут все наши тесты.
package.json
Создайте файл package.json и вставьте следующее:
< "name": "bookstore", "version": "1.0.0", "description": "A bookstore API", "main": "server.js", "author": "Sam", "license": "ISC", "dependencies": < "body-parser": "^1.15.1", "config": "^1.20.1", "express": "^4.13.4", "mongoose": "^4.4.15", "morgan": "^1.7.0" >, "devDependencies": < "chai": "^3.5.0", "chai-http": "^2.0.1", "mocha": "^2.4.5" >, "scripts": < "start": "SET NODE_ENV=dev && node server.js", "test": "mocha --timeout 10000" >>Опять-таки, ничего нового для того, кто написал хотя бы один сервер на node.js. Пакеты mocha , chai , chai-http , необходимые для тестирования, устанавливаются в блок dev-dependencies (флаг —save-dev из командной строки).
Блок scripts содержит два способа запуска сервера.Для mocha я добавил флаг —timeout 10000 , потому что я забираю данные из базы, расположенной на mongolab и отпущенные двух секунд по умолчанию может не хватать.
Ура! Мы закончили скучную часть руководства и настало время написать сервер и протестировать его.
Сервер
Давайте создадим файл server.js и вставим следующий код:
let express = require('express'); let app = express(); let mongoose = require('mongoose'); let morgan = require('morgan'); let bodyParser = require('body-parser'); let port = 8080; let book = require('./app/routes/book'); let config = require('config'); // загружаем адрес базы из конфигов //настройки базы let options = < server: < socketOptions: < keepAlive: 1, connectTimeoutMS: 30000 >>, replset: < socketOptions: < keepAlive: 1, connectTimeoutMS : 30000 >> >; //соединение с базой mongoose.connect(config.DBHost, options); let db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); //не показывать логи в тестовом окружении if(config.util.getEnv('NODE_ENV') !== 'test') < //morgan для вывода логов в консоль app.use(morgan('combined')); //'combined' выводит логи в стиле apache >//парсинг application/json app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); app.use(bodyParser.text()); app.use(bodyParser.json(< type: 'application/json'>)); app.get("/", (req, res) => res.json()); app.route("/book") .get(book.getBooks) .post(book.postBook); app.route("/book/:id") .get(book.getBook) .delete(book.deleteBook) .put(book.updateBook); app.listen(port); console.log("Listening on port " + port); module.exports = app; // для тестирования- Нам нужен модуль config для доступа к файлу конфигурации в соответствии с переменной окружения NODE_ENV. Из него мы получаем mongo db URI для соединения с базой данных. Это позволит нам содержать основную базу чистой, а тесты проводить на отдельной базы, скрытой от пользователей.
- Переменная окружения NODE_ENV проверяется на значение «test», чтобы отключить логи morgan в командной строке, иначе они появятся в выводе при запуске тестов.
- Последняя строка экспортирует сервер для тестов.
- Обратите внимание на объявление переменных через let . Оно делает переменную видимой только в рамках замыкающего блока или глобально, если она вне блока.
В остальное ничего нового: мы просто подключаем нужные модули, определяем настройки для взаимодействия с сервером, создаем точки входа и запускаем сервер на определенном порту.
Модели и роутинг
Настало время для описать модель книги. Создадим файл book.js в папке /app/model/ со следующим содержимым:
let mongoose = require('mongoose'); let Schema = mongoose.Schema; //определение схемы книги let BookSchema = new Schema( < title: < type: String, required: true >, author: < type: String, required: true >, year: < type: Number, required: true >, pages: < type: Number, required: true, min: 1 >, createdAt: < type: Date, default: Date.now >, >, < versionKey: false >); // установить параметр createdAt равным текущему времени BookSchema.pre('save', next => < now = new Date(); if(!this.createdAt) < this.createdAt = now; >next(); >); //Экспорт модели для последующего использования. module.exports = mongoose.model('book', BookSchema);У нашей книги есть название, автор, количество страниц, год публикации и дата создания в базе. Я установил опции versionKey значение false , так как она не нужна в данном руководстве.
Необычный callback в .pre() — это функция стрелка, функция с более коротким синтаксисом. Согласно определению MDN: «привязывается к текущему значению this (не имеет собственного this , arguments , super , or new.target ). Функции-стрелки всегда анонимны».
Отлично, теперь мы знаем все что нужно о модели и переходим к роутам.
В папке /app/routes/ создадим файл book.js следующего содержания:
let mongoose = require('mongoose'); let Book = require('../models/book'); /* * GET /book маршрут для получения списка всех книг. */ function getBooks(req, res) < //Сделать запрос в базу и, если не ошибок, отдать весь список книг let query = Book.find(<>); query.exec((err, books) => < if(err) res.send(err); //если нет ошибок, отправить клиенту res.json(books); >); > /* * POST /book для создания новой книги. */ function postBook(req, res) < //Создать новую книгу var newBook = new Book(req.body); //Сохранить в базу. newBook.save((err,book) => < if(err) < res.send(err); >else < //Если нет ошибок, отправить ответ клиенту res.json(); > >); > /* * GET /book/:id маршрут для получения книги по ID. */ function getBook(req, res) < Book.findById(req.params.id, (err, book) =>< if(err) res.send(err); //Если нет ошибок, отправить ответ клиенту res.json(book); >); > /* * DELETE /book/:id маршрут для удаления книги по ID. */ function deleteBook(req, res) < Book.remove(, (err, result) => < res.json(< message: "Book successfully deleted!", result >); >); > /* * PUT /book/:id маршрут для редактирования книги по ID */ function updateBook(req, res) < Book.findById(, (err, book) => < if(err) res.send(err); Object.assign(book, req.body).save((err, book) =>< if(err) res.send(err); res.json(< message: 'Book updated!', book >); >); >); > //экспортируем все функции module.exports = < getBooks, postBook, getBook, deleteBook, updateBook >;- Все маршруты стандартные GET, POST, DELETE, PUT для выполнение CRUD.
- В функции updatedBook() мы используем Object.assign , новую функцию ES6, которая перезаписывает общие свойства book и req.body и оставляет.остальные нетронутыми
- В конце мы экспортируем объект с использованием синтаксиса «короткое свойство» (на русском можно почитать тут, прим. переводчика) чтобы не делать повторений.
Мы закончили эту часть и получили готовое приложение!
Наивное тестирование
Давайте запустим наше приложение, откроем POSTMAN для отправки HTTP запросов к серверу и проверим что все работает как ожидалось.
В командной строке выпоним
npm startGET /BOOK

В POSTMAN выполним GET запрос и, если предположить что в базе есть книги, получим ответ:
:Сервер без ошибок вернул книги из базы.
POST /BOOK
Давайте добавим новую книгу:

Похоже, что книга добавилась. Сервер вернул книгу и сообщение, подтверждающее, что она была добавлена. Так ли это? Выполним еще один GET запрос и посмотрим на результат:

PUT /BOOK/:ID
Давайте поменяем количество страниц в книге и посмотрим на результат:

Отлично! PUT тоже работает, так что можно выполнить еще один GET запрос для проверки

GET /BOOK/:ID
Теперь получим одну книгу по ID в GET запросе и потом удалим ее:

Получили правильный ответ и теперь удалим эту книгу:
DELETE /BOOK/:ID
Посмотрим на результат удаления:

Даже последний запрос работает как и задумано и нам даже не нужно делать еще один GET запрос для проверки, так как мы отправили клиенту ответ от mongo (свойство result), которое показывает, что книга действительно удалилась.
При выполнении тестом через POSTMAN приложение ведет себя как и ожидается, верно? Значит, его можно можно использовать на клиенте?
Давайте я вам отвечу: НЕТ!!
Наши действия я называю наивным тестированием, потому что мы выполнили только несколько операций без учета спорных случаев: POST запрос без ожидаемых данных, DELETE с неверным id или вовсе без id.
Очевидно это простое приложение и, если нам повезло, мы не наделали ошибок, но как насчет реальных приложений? Более того, мы потратили время на запуск в POSTMAN некоторых тестовых HTTP запросов. А что случится, если однажды мы решим изменить код одного из них? Опять все проверять в POSTMAN?
Это только несколько ситуаций, с которыми вы можете столкнуться или уже столкнулись как разработчик. К счастью, у нас есть инструменты, позволяющие создать тесты, которые всегда доступны; их можно запустить одной командной из консоли.
Давайте сделаем что-то лучшее, чтобы тестировать наше приложение.
Хорошее тестирование
Во-первых, давайте создадим файл books.js в папке /test :
//During the test the env variable is set to test process.env.NODE_ENV = 'test'; let mongoose = require("mongoose"); let Book = require('../app/models/book'); //Подключаем dev-dependencies let chai = require('chai'); let chaiHttp = require('chai-http'); let server = require('../server'); let should = chai.should(); chai.use(chaiHttp); //Наш основной блок describe('Books', () => < beforeEach((done) =>< //Перед каждым тестом чистим базу Book.remove(<>, (err) => < done(); >); >); /* * Тест для /GET */ describe('/GET book', () => < it('it should GET all the books', (done) => < chai.request(server) .get('/book') .end((err, res) =>< res.should.have.status(200); res.body.should.be.a('array'); res.body.length.should.be.eql(0); done(); >); >); >); >);Как много новых штук! Давай разберемся:
- Обязательно обратите внимание на переменную NODE_ENV которой мы присвоили значение test. Это позволит серверу загрузить конфиг для тестовой базы и не выводить в консоль логи morgan .
- Мы подключили dev-dependencies и собственно сервер (мы его экспортировали через module.exports).
- Мы подключили chaiHttp к chai .
Все начинается с блока describe , который используется для улучшения структуризации наших утверждений. Это отразится на выводе, как мы увидим позже.
beforeEach — это блок, который выполнится для каждого блока описанного в этом describe блоке. Для чего мы это делаем? Мы удаляем все книги из базы, чтобы база была пуста в начале каждого тесте.
Тестируем /GET
Итак, у нас есть первый тест. Chai выполняет GET запрос и проверяет, что переменная res удовлетворяет первому параметру (утверждение) блока it «it should GET all the books». А именно, для данного пустого книгохранилища ответ должен быть следующим:
- Статус 200.
- Результат должен быть массивом.
- Так как база пуста, мы ожидаем что размер массива будет равен 0.
Обратите внимание, что синтаксис should интуитивен и очень похож на разговорный язык.
Терерь в командной строке выпоним:
npm test
и получим:
Тест прошел и вывод отражает структуру, которую мы описали с помощью блоков describe .
Тестируем /POST
Теперь проверим насколько хорош наш API. Предположим мы пытаемся добавить книгу без поля `pages: сервер не должен вернуть соответствующую ошибку.
Добавим этот код в конец блока describe(‘Books’) :
describe('/POST book', () => < it('it should not POST a book without pages field', (done) => < let book = < title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954 >chai.request(server) .post('/book') .send(book) .end((err, res) => < res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('errors'); res.body.errors.should.have.property('pages'); res.body.errors.pages.should.have.property('kind').eql('required'); done(); >); >); >);Тут мы добавили тест на неполный /POST запрос. Посмотрим на проверки:
- Статус должен быть 200.
- Тело ответа должно быть объектом.
- Одним из свойств тела ответа должно быть errors .
- У поля errors должно быть пропущенное в запросе свойство pages .
- pages должно иметь свойство kind равное required чтобы показать причину почему мы получили негативный ответ от сервера.
Обратите внимание, что мы отправили данные о книге с помощью метода .send().
Давайте выполним команду еще раз и посмотрим на вывод:

Перед тем, как писать следующий тест, уточним пару вещей:
- Во-первых, почему ответ от сервера имеет такую структуру? Если вы читали callback для маршрута /POST, то вы увидели что в случае ошибки сервер отправляет в ответ ошибку от mongoose . Попробуйте сделать это через POSTMAN и посмотрите на ответ.
- В случае ошибки мы все равно отвечаем с кодом 200. Это сделано для простоты, так как мы только учимся тестировать наш API.
Однако я бы предложил отдавать в ответ статус 206 Partial Content instead
Давайте отправим правильный запрос. Вставьте следующий код в конец блока describe(»/POST book») :
it('it should POST a book ', (done) => < let book = < title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 >chai.request(server) .post('/book') .send(book) .end((err, res) => < res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Book successfully added!'); res.body.book.should.have.property('title'); res.body.book.should.have.property('author'); res.body.book.should.have.property('pages'); res.body.book.should.have.property('year'); done(); >); >);На этот раз мы ожидаем объект, говорящий нам, что книга добавилась успешно и собственно книгу. Вы уже должны быть хорошо знакомы с проверками, так что нет нужды вдаваться в детали.
Снова запустим команду и получим:

Тестируем /GET/:ID
Теперь создадим книгу, сохраним ее в базу и используем id для выполнения GET запроса. Добавим следующий блок:
describe('/GET/:id book', () => < it('it should GET a book by the given id', (done) =>< let book = new Book(< title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 >); book.save((err, book) => < chai.request(server) .get('/book/' + book.id) .send(book) .end((err, res) =>< res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('title'); res.body.should.have.property('author'); res.body.should.have.property('pages'); res.body.should.have.property('year'); res.body.should.have.property('_id').eql(book.id); done(); >); >); >); >);Через asserts мы убедились, что сервер возвратил все поля и нужную книгу (id в ответе от севера совпадает с запрошенным):

Вы заметили, что в тестированием отдельных маршрутов внутри независимых блоков мы получили очень чистый вывод? К томе же это эффективно: мы написали несколько тестов, которые можно повторить с помощью одной команды
Тестируем /PUT/:ID
Настало время проверить редактирование одной из наших книг. Сначала мы сохраним книгу в базу, а потом выпоним запрос, чтобы поменять год ее публикации.
describe('/PUT/:id book', () => < it('it should UPDATE a book given the id', (done) =>< let book = new Book() book.save((err, book) => < chai.request(server) .put('/book/' + book.id) .send() .end((err, res) => < res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Book updated!'); res.body.book.should.have.property('year').eql(1950); done(); >); >); >); >);Мы хотим убедиться, что поле message равно Book updated! и поле year действительно изменилось.

Мы почти закончили.
Шаблон очень похож на предыдущий тест: сначала создаем книгу, потом ее удаляем с помощью запроса и проверяем ответ:
describe('/DELETE/:id book', () => < it('it should DELETE a book given the id', (done) =>< let book = new Book() book.save((err, book) => < chai.request(server) .delete('/book/' + book.id) .end((err, res) =>< res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Book successfully deleted!'); res.body.result.should.have.property('ok').eql(1); res.body.result.should.have.property('n').eql(1); done(); >); >); >); >);Снова сервер вернёт нам ответ от mongoose , который мы и проверяем. В консоли будет следующее:

Восхитительно! Наши тесты проходят и у нас есть отличная база для тестирования нашего API с помощью более изысканных проверок.
Заключение
В этом уроке мы столкнулись с проблемой тестирования наших маршрутов, чтобы предоставить нашим пользователям стабильный API.
Мы прошли через все этапы создания RESTful API, делая наивные тесты с POSTMAN, а затем предложили лучший способ тестирования, являлось нашей основной целью.
Написание тестов является хорошей привычкой для обеспечения стабильности работы сервера. К сожалению часто это недооценивается.
Бонус: Mockgoose
Всегда найдется кто-то, кто скажет что две базы — это не лучшее решение, но другого не дано. И что же делать? Альтернатива есть: Mockgoose.
По сути Mockgoose создает обертку для Mongoose, которая перехватывает обращения к базе и вместо этого использует in memory хранилище. К тому же он легко интегрируется с mocha
Примечание: Mockgoose требует чтобы на машине, где запускаются тесты была установлена mongodb
