Русский
Русский
English
Статистика
Реклама

Express

Перевод Выбираем лучший бэкенд-фреймворк 2021 года

18.09.2020 16:04:09 | Автор: admin
Недавно мы опубликовали статью, автор которой размышлял о том, какой язык программирования, JavaScript, Python или Go, лучше всего подойдёт для бэкенд-разработки в 2021 году. Сегодня мы представляем вашему вниманию перевод ещё одного материала того же автора. Здесь он пытается найти ответ на вопрос о том, на какой фреймворк для разработки серверных частей приложений стоит обратить внимание тем, кто хочет во всеоружии встретить 2021 год.



О роли фреймворков в IT-индустрии


Я, проанализировав результаты опроса разработчиков, проведённого в 2020 году Stack Overflow, понял, что использование фреймворков играет огромную роль в IT-индустрии. А именно, речь идёт об ответах на вопрос о том, что разработчики считают самым важным при поиске работы.


Факторы, влияющие на выбор места работы

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

Разработчик обычно знаком хотя бы с одним фреймворком. Здесь я собираюсь рассказать о трёх фреймворках о Node.js/Express, Django и Spring Boot. Полагаю, что тот, кто, готовясь к 2021 году, решит сделать своим основным инструментом один из них, в любом случае, не прогадает. Но у каждого из них есть свои особенности, которые мне и хотелось бы здесь обсудить.

Результаты исследований и другие данные


Данные с GitHut


Ресурс GitHut позволяет узнать различные сведения о репозиториях. В частности, речь идёт о количестве активных репозиториев, об общем количестве PR, и о разных показателях, рассчитываемых по состоянию на один репозиторий: количество PR и форков, число открытых задач, количество новых подписчиков.


Популярность языков программирования на GitHub

Исследование Stack Overflow


Если взглянуть на результаты исследования Stack Overflow, то окажется, что Express находится на первом месте среди бэкенд-фреймворков, которые любят разработчики. Два других интересующих нас фреймворка, Spring и Django, следуют за Express с небольшим отрывом. В результате оказывается, что проект, основанный на самом перспективном скриптовом языке, на JavaScript, лидирует, а за ним идёт проект, в котором используется один из языков, обладающих самыми широкими возможностями, то есть Python.


Популярность веб-фреймворков

GitHub-репозитории


Некоторые люди не любят перемен, но изменения необходимо принимать, если альтернативой является катастрофа.
Илон Маск


Фреймворк Express создан на базе платформы Node.js, поэтому тут мы будем сравнивать репозитории Node.js, Spring Boot и Django


Репозиторий node


Репозиторий spring-boot


Репозиторий django

Как видно, больше всего звёзд у репозитория Node.js. Но разница между звёздами проектов не так сильна, как различие в количестве их форков. У Spring Boot и Django имеется гораздо больше форков, чем у Node.js.

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

Node.js/Express


Node.js представляет собой серверную платформу, которая является частью стека технологий, охватывающих все нужды веб-разработки, и основанных на JavaScript. В Node.js используется JavaScript-движок V8, тот же самый, что применяется в браузере Chrome и в других браузерах, основанных на Chromium. В результате оказывается, что благодаря использованию Node.js код, предназначенный для выполнения на сервере, можно писать на JavaScript. На базе платформы Node.js создано множество фреймворков, включая такие популярные, как Express.

Сильные стороны Node.js


  • Появление Node.js сделало возможным фуллстек-разработку веб-проектов на JavaScript. В результате в распоряжении разработчиков серверных частей приложений оказались и сильные возможности JavaScript, и наработки экосистемы JS, библиотеки, которыми стало реально воспользоваться в серверном окружении.
  • JavaScript-код, аналогичный по функционалу, например, коду, написанному на C, оказывается компактнее. Производительность JavaScript-кода при этом достаточно высока для применения его в проектах, в которых важна скорость работы кода.
  • Код клиентских и серверных частей проектов легче поддерживать в согласованном состоянии, так как и там и там используется один и тот же язык.
  • Один и тот же код можно совместно использовать и на клиенте, и на сервере.
  • Благодаря существованию модулей Node.js, которые, в сущности, представляют собой особым образом оформленные фрагменты кода, разработчики могут с удобством использовать в своих проектах чужой код, а так же собственные наработки.
  • Платформа Node.js, и, соответственно, основанные на ней фреймворки, отличаются нетребовательностью к ресурсам и масштабируемостью. Именно поэтому Node.js это платформа, к которой часто прибегают те, кто пользуется микросервисными архитектурами.
  • Эта платформа хорошо подходит для разработки микросервисов ещё и из-за существования системы модулей Node.js, которые можно представить себе в виде строительных блоков серверных приложений.
  • В Node.js JavaScript код компилируется в машинный код, что позволяет получить гораздо более высокую производительность, чем при интерпретации кода. Сообщество JavaScript-разработчиков видит постоянное улучшение производительности Node.js за счёт того, что Google постоянно работает над совершенствованием V8.
  • Благодаря тому, что в Node.js имеется система ввода-вывода, не блокирующая главный поток, эта платформа демонстрирует высокую производительность. Достойная скорость обработки запросов достигается благодаря использованию JavaScript-механизмов конкурентного однопоточного выполнения кода.
  • Node.js это опенсорсный проект, вокруг которого собралось огромное сообщество разработчиков. Это значит, что тот, кто столкнётся с какой-то проблемой, сможет достаточно быстро найти её решение.
  • Node.js, в ближайшем будущем, может стать платформой, которую будут использовать для проведения тяжёлых вычислений, наподобие тех, которые применяются для решения задач машинного обучения.

Компании, которые используют Node.js


  • Paypal
  • Netflix
  • LinkedIn
  • Uber
  • eBay
  • Yahoo

Если учесть сильные стороны Node.js и то, в каких компаниях используется эта платформа, становятся понятными причины её огромной популярности. Зарплата Node.js-разработчиков в США варьируется в пределах $40,000-$130,000 в год. В результате можно сказать, что если вы чувствуете, что Node.js и фреймворки, основанные на этой платформе, вам интересны, вы вполне можете выбрать именно их в качестве базы для своих разработок 2021 года.

Spring Boot


Проект Spring Boot это фреймворк для разработки бэкенд-приложений, основанный на Java, который, как и Node.js, используется для разработки микросервисов. Этот фреймворк упрощает создание приложений, основанных на Spring, его можно представить себе в виде инструмента для создания самостоятельных Spring-приложений. Если вы планируете в 2021 году перейти на Spring, то вам, определённо, стоит знать о том, чем вам в этом деле сможет помочь Spring Boot.

Сильные стороны Spring Boot


  • Spring Boot позволяет с минимальными усилиями создавать самостоятельные Spring-приложения, облегчает процесс их конфигурирования, упрощает работу над ними. Подобные приложения легко запускаются с помощью команды java -jar.
  • Если в процессе создания Spring Boot-приложения произошла ошибка, встроенный анализатор ошибок поможет справиться с проблемой.
  • Spring Boot поддерживает встроенные серверы, вроде Tomcat и Jetty. Это значит, что тем, кто пользуется Spring Boot, не нужно развёртывать .war-файлы на внешних серверах.
  • Использование Spring Boot позволяет облегчить конфигурирование Maven за счёт наличия в системе начальных вариантов файла pom.xml.
  • В возможности фреймворка входит автоматическое конфигурирование Spring.
  • Spring Boot хорошо интегрируется с другими фреймворками.
  • Фреймворк предоставляет разработчику конфигурации, готовые для продакшн-использования. Сюда входят, например, метрики состояния проекта и внешние конфигурации.
  • При использовании Spring Boot нет нужды в применении XML-конфигураций или средств для генерирования кода.
  • Применение Spring Boot облегчает труд разработчиков за счёт применения принципа проектирования ПО, известного как Convention over Configuration.

Компании, которые используют Spring


  • Platform
  • Intuit
  • MIT
  • Zillow
  • TransferWise

Честно говоря, я не фанат Java. И я не будут использовать Spring Boot для серверной разработки в 2021 году. Но, если верить статистике, существует много программистов, применяющих этот фреймворк. Если говорить о зарплатах соответствующих специалистов, то это что-то около $50,000-$104,000 в год. Это немного меньше, чем зарплаты Node.js-разработчиков.

Django


Django это опенсорсный бэкенд-фреймворк, написанный на Python. Как известно, Python это один из таких языков, которые пользуются наибольшей любовью разработчиков. И это одна из основных причин того, что Django является одним из самых популярных серверных фреймворков. Но у того, чтобы выбрать Django в качестве своего фреймворка 2021 года, есть и другие причины.

Сильные стороны Django


  • Django позволяет без особых сложностей создавать динамические веб-приложения с использованием Python. Данный фреймворк написан на Python. В этом заключается одно из главных достоинств Django.
  • Фреймворк поддерживает паттерн проектирования MVC. Это помогает разработчикам в разделении пользовательского интерфейса и бизнес-логики Django-приложений.
  • Это быстрый фреймворк, не перегруженный ненужными возможностями. Я имеют в виду то, что использование Django позволяет быстро выйти на работоспособный проект.
  • Django не относится к минималистичным фреймворкам, широко используемым для разработки микросервисов. Он отличается мощностью, универсальностью и определённым своеобразием.
  • Создатели этого фреймворка серьёзно относятся к безопасности. Поэтому они дают разработчикам, использующим Django, соответствующие инструменты. Я уверен, что все вы знаете о том, как много проблем существует в наши дни в сфере кибербезопасности. Поэтому чрезвычайно важными являются вопросы защиты веб-проектов. Django поддерживает систему аутентификации пользователей, содержит инструменты для защиты от различных атак. Среди них средства защиты от SQL-инъекций, от межсайтового скриптинга, от межсайтовой подделки запросов, от кликджекинга.
  • Django-проекты отличаются компактностью кода.
  • Разработчики, использующие Django, могут моделировать базовые классы. Это значит, что в их распоряжении всегда имеется ORM.
  • Django это кросс-платформенный проект. Он отлично работает на различных операционных системах. Кроме того, он поддерживает взаимодействие с различными базами данных.
  • Это фреймворк, приложения, созданные с использованием которого, хорошо поддаются масштабированию. Поэтому тот, кто выбирает Django, может быть уверенным в том, что сможет эффективно развивать свой проект по мере его роста.
  • Вокруг Django сформировалось активное сообщество. Поэтому тот, кто столкнулся с какой-то проблемой, сможет без особых сложностей её решить.

Компании, которые используют Django


  • Mozilla
  • NASA
  • Pinterest
  • Bitbucket
  • Instagram

Так как Django основан на Python, о производительности этого фреймворка и о его поддержке можно не беспокоиться. Кроме того, если взглянуть на список компаний, использующих Django, можно сделать вывод о том, что это фреймворк, достойный внимания и в этом, и в будущем году. Если проанализировать зарплаты Django-разработчиков, то окажется, что это что-то между $90,000-$120,000 в год. В результате оказывается, что спрос на Django-специалистов достаточно высок.

Итоги


Вышеприведённые факты позволяют сделать вывод о том, что платформа Node.js, в лице фреймворка Express, и Django показывают себя очень хорошо. Но, в то же время, Spring Boot тоже представляет собой достаточно интересное явление. Полагаю, что самым интересной платформой для разработки серверных частей приложений в 2021 году будет Node.js. Хотя и Django это отличный, развитый фреймворк, который вполне может стать чьим-то выбором в будущем году.

Каким фреймворком для разработки серверных частей веб-проектов вы планируете пользоваться в 2021 году?



Подробнее..

Перевод Наилучшие практики создания REST API

17.07.2020 10:14:14 | Автор: admin
Всем привет!

Предлагаемая вашему вниманию статья, несмотря на невинное название, спровоцировала на сайте Stackoverflow столь многословную дискуссию, что мы не смогли пройти мимо нее. Попытка объять необъятное внятно рассказать о грамотном проектировании REST API по-видимому, удалась автору во многом, но не вполне. В любом случае, надеемся потягаться с оригиналом в градусе обсуждения, а также на то, что пополним армию поклонников Express.

Приятного чтения!


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

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

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

В этой статье будет рассмотрено, как проектировать REST API таким образом, чтобы они были просты и понятны для каждого, кто их потребляет. Обеспечим их долговечность, безопасность и скорость, так как данные, передаваемые клиентам через такой API, могут быть конфиденциальными.

Поскольку существует множество причин и вариантов отказа сетевого приложения, мы должны убедиться, что ошибки в любом REST API будут обрабатываться изящно и сопровождаться стандартными HTTP-кодами, которые помогут потребителю разобраться с проблемой.

Принимаем JSON и выдаем JSON в ответ

REST API должны принимать JSON для полезной нагрузки запроса, а также отправлять отклики в формате JSON. JSON это стандарт передачи данных. К его использованию приспособлена практически любая сетевая технология: в JavaScript есть встроенные методы для кодирования и декодирования JSON либо через Fetch API, либо через другой HTTP-клиент. В серверных технологиях используются библиотеки, позволяющие декодировать JSON практически без вмешательства с вашей стороны.

Существуют и другие способы передачи данных. Язык XML как таковой не очень широко поддерживается во фреймворках; обычно требуется преобразование данных в более удобный формат, а это обычно JSON. На стороне клиента, особенно в браузере, не так легко обращаться с этими данными. Приходится выполнять массу дополнительной работы всего лишь для того, чтобы обеспечить нормальную передачу данных.

Формы удобны для передачи данных, особенно если мы собираемся пересылать файлы. Но для передачи информации в текстовом и числовом виде можно обойтись без форм, поскольку в большинстве фреймворков допускается передача JSON без дополнительной обработки достаточно взять данные на стороне клиента. Это наиболее прямолинейный способ обращения с ними.

Чтобы гарантировать, что клиент интерпретирует JSON, полученный с нашего REST API, именно как JSON, следует установить для Content-Type в заголовке отклика значение application/json после того, как будет сделан запрос. Многие серверные фреймворки приложений устанавливают заголовок отклика автоматически. Некоторые HTTP-клиенты смотрят Content-Type в заголовке отклика и разбирают данные в соответствии с указанным там форматом.

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

Также следует убедиться, что в отклике от наших конечных точек нам приходит именно JSON. Во многих серверных фреймворках данная возможность является встроенной.
Рассмотрим в качестве примера API, принимающий полезную нагрузку в формате JSON. В данном примере используется бэкендовый фреймворк Express для Node.js. Можно использовать в качестве промежуточного ПО программу body-parser для разбора тела запроса JSON, а затем вызвать метод res.json с объектом, который мы хотим вернуть в качестве отклика JSON. Это делается так:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.post('/', (req, res) => {  res.json(req.body);});app.listen(3000, () => console.log('server started'));


bodyParser.json() разбирает строку с телом запроса в JSON, преобразуя ее в объект JavaScript, а затем присваивает результат объекту req.body.

Установим для заголовка Content-Type в отклике значение application/json; charset=utf-8 без каких-либо изменений. Метод, показанный выше, применим и в большинстве других бэкендовых фрейморков.

В названиях путей к конечным точкам используем имена, а не глаголы

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

Дело в том, что в названии нашего метода HTTP-запроса уже содержится глагол. Ставить глаголы в названиях путей к конечной точке API нецелесообразно; более того, имя получается излишне длинным и не несет никакой ценной информации. Глаголы, выбираемые разработчиком, могут ставиться просто в зависимости от его прихоти. Например, кому-то больше нравится вариант get, а кому-то retrieve, поэтому лучше ограничиться привычным глаголом HTTP GET, сообщающим, что именно делает конечная точка.

Действие должно быть указано в названии HTTP-метода того запроса, который мы выполняем. В названиях наиболее распространенных методов содержатся глаголы GET, POST, PUT и DELETE.
GET извлекает ресурсы. POST отправляет новые данные на сервер. PUT обновляет имеющиеся данные. DELETE удаляет данные. Каждый из этих глаголов соответствует одной из операций из группы CRUD.

Учитывая два принципа, рассмотренных выше, для получения новых статей мы должны создавать маршруты вида GET /articles/. Аналогично, используем POST /articles/ для обновления новой статьи, PUT /articles/:id для обновления статьи с заданным id. Метод DELETE /articles/:id предназначен для удаления статьи с заданным ID.

/articles это ресурс REST API. Например, можно воспользоваться Express, чтобы выполнять со статьями следующие операции:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/articles', (req, res) => {  const articles = [];  // код для извлечения статьи...  res.json(articles);});app.post('/articles', (req, res) => {  // код для добавления новой статьи...  res.json(req.body);});app.put('/articles/:id', (req, res) => {  const { id } = req.params;  // код для обновления статьи...  res.json(req.body);});app.delete('/articles/:id', (req, res) => {  const { id } = req.params;  // код для удаления статьи...  res.json({ deleted: id });});app.listen(3000, () => console.log('server started'));


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

Конечные точки POST, PUT и DELETE принимают тело запроса в формате JSON и возвращают отклик также в формате JSON, включая в него конечную точку GET.

Коллекции называем существительными во множественном числе

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

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

При работе с конечной точкой /articles мы пользуемся множественным числом при именовании всех конечных точек.

Вложение ресурсов при работе с иерархическими объектами

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

Например, если мы хотим на определенной конечной точке получать комментарии к новой статье, то должны прикрепить путь /comments к концу пути /articles. В данном случае предполагается, что мы считаем сущность comments дочерней для article в нашей базе данных.

Например, это можно сделать при помощи следующего кода в Express:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/articles/:articleId/comments', (req, res) => {  const { articleId } = req.params;  const comments = [];  // код для получения комментариев по articleId  res.json(comments);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде можно использовать метод GET в пути '/articles/:articleId/comments'. Мы получаем комментарии comments к статье, которой соответствует articleId, а затем возвращаем ее в ответ. Мы добавляем 'comments' после сегмента пути '/articles/:articleId', чтобы указать, что это дочерний ресурс /articles.

Это логично, поскольку comments являются дочерними объектами articles и предполагается, что у каждой статьи свой набор комментариев. В противном случае данная структура может запутать пользователя, поскольку обычно применяется для доступа к дочерним объектам. Тот же принцип действует при работе с конечными точками POST, PUT и DELETE. Все они используют одно и то же вложение структур при составлении имен путей.

Аккуратная обработка ошибок и возврат стандартных кодов ошибок

Чтобы избавиться от путаницы, когда на API происходит ошибка, нужно обрабатывать ошибки аккуратно и возвращать коды отклика HTTP, указывающие, какая именно ошибка произошла. Таким образом те, кто поддерживает API, получают достаточную информацию для понимания возникшей проблемы. Недопустимо, чтобы ошибки обваливали систему, поэтому и без обработки их оставлять нельзя, и заниматься такой обработкой должен потребитель API.
Коды наиболее распространенных ошибок HTTP:

  • 400 Bad Request (Плохой Запрос) означает, что ввод, полученный с клиента, не прошел валидацию.
  • 401 Unauthorized (Не авторизован) означает, что пользователь не представился и поэтому не имеет права доступа к ресурсу. Обычно такой код выдается, когда пользователь не прошел аутентификацию.
  • 403 Forbidden (Запрещено) означает, что пользователь прошел аутентификацию, но не имеет права на доступ к ресурсу.
  • 404 Not Found (Не найдено) означает, что ресурс не найден
  • 500 Internal server error (Внутренняя ошибка сервера) это ошибка сервера, которую, вероятно, не следует выбрасывать явно.
  • 502 Bad Gateway (Ошибочный шлюз) означает недействительное ответное сообщение от вышестоящего сервера.
  • 503 Service Unavailable (Сервис недоступен) означает, что на стороне сервера произошло нечто непредвиденное например, перегрузка сервера, отказ некоторых элементов системы, т.д.


Следует выдавать именно такие коды, которые соответствуют ошибке, помешавшей нашему приложению. Например, если мы хотим отклонить данные, пришедшие в качестве полезной нагрузки запроса, то, в соответствии с правилами Express API, должны вернуть код 400:

const express = require('express');const bodyParser = require('body-parser');const app = express();// существующие пользователиconst users = [  { email: 'abc@foo.com' }]app.use(bodyParser.json());app.post('/users', (req, res) => {  const { email } = req.body;  const userExists = users.find(u => u.email === email);  if (userExists) {    return res.status(400).json({ error: 'User already exists' })  }  res.json(req.body);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде мы держим в массиве users список имеющихся пользователей, у которых известна электронная почта.

Далее, если мы пытаемся передать полезную нагрузку со значением email, уже присутствующим в users, то получаем отклик с кодом 400 и сообщение 'User already exists', означающее, что такой пользователь уже существует. Располагая этой информацией, пользователь может поправиться заменить адрес электронной почты на тот, которого пока нет в списке.

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

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

Разрешать сортировку, фильтрацию и разбивку данных на страницы

Базы, расположенные за REST API, могут сильно разрастаться. Иногда данных бывает настолько много, что все их невозможно вернуть за один раз, так как это замедлит систему или вообще обрушит ее. Следовательно, нам нужен способ фильтрации элементов.

Также необходимы способы разбивки данных на страницы (пагинации), чтобы за один раз мы возвращали всего несколько результатов. Мы не хотим занимать ресурсы слишком надолго, пытаясь вытянуть все запрошенные данные разом.

Как фильтрация, так и пагинация данных позволяют повысить производительность, сократив использование серверных ресурсов. Чем больше данных накапливается в базе, тем более важными становятся две эти возможности.

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

const express = require('express');const bodyParser = require('body-parser');const app = express();// информация о сотрудниках в базе данныхconst employees = [  { firstName: 'Jane', lastName: 'Smith', age: 20 },  //...  { firstName: 'John', lastName: 'Smith', age: 30 },  { firstName: 'Mary', lastName: 'Green', age: 50 },]app.use(bodyParser.json());app.get('/employees', (req, res) => {  const { firstName, lastName, age } = req.query;  let results = [...employees];  if (firstName) {    results = results.filter(r => r.firstName === firstName);  }  if (lastName) {    results = results.filter(r => r.lastName === lastName);  }  if (age) {    results = results.filter(r => +r.age === +age);  }  res.json(results);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде у нас есть переменная req.query, позволяющая получить параметры запроса. Затем мы можем извлечь значения свойств путем деструктуризации отдельных параметров запроса в переменные; для этого в JavaScript предусмотрен специальный синтаксис.
Наконец, мы применяем filter с каждым значением параметра запроса, чтобы найти те элементы, которые хотим вернуть.

Справившись с этим, возвращаем results в качестве отклика. Следовательно, при выполнении запроса GET к следующему пути со строкой запроса:

/employees?lastName=Smith&age=30

Получаем:
[    {        "firstName": "John",        "lastName": "Smith",        "age": 30    }]


в качестве возвращенного ответа, поскольку фильтрация производилась по lastName и age.
Аналогично, можно принять параметр запроса page и вернуть группу записей, занимающих позиции от (page - 1) * 20 до page * 20.

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

http://example.com/articles?sort=+author,-datepublished

Где + означает вверх, а вниз. Таким образом, мы сортируем по имени автора в алфавитном порядке и по datepublished от новейшего к наиболее давнему.

Придерживаться проверенных практик обеспечения безопасности

Коммуникация между клиентом и сервером должна быть в основном приватной, так как зачастую мы отправляем и получаем конфиденциальную информацию. Следовательно, использование SSL/TLS для обеспечения безопасности обязательное условие.

Сертификат SSL не так сложно загрузить на сервер, а сам этот сертификат либо бесплатен, либо стоит очень дешево. Нет никаких причин отказываться от того, чтобы обеспечивать коммуникацию наших REST API по защищенным каналам, а не по открытым.

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

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

Кэшировать данные для улучшения производительности

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

Существуют разнообразные варианты решений для кэширования, например, Redis, кэширование в оперативной памяти и многие другие. По мере надобности можно менять способ кэширования данных.

Например, в Express предусмотрено промежуточное ПО apicache, позволяющее добавить в приложение возможность кэширования без сложной настройки конфигурации. Простое кэширование в оперативной памяти можно добавить на сервер вот так:

const express = require('express');const bodyParser = require('body-parser');const apicache = require('apicache');const app = express();let cache = apicache.middleware;app.use(cache('5 minutes'));// информация о сотрудниках в базе данныхconst employees = [  { firstName: 'Jane', lastName: 'Smith', age: 20 },  //...  { firstName: 'John', lastName: 'Smith', age: 30 },  { firstName: 'Mary', lastName: 'Green', age: 50 },]app.use(bodyParser.json());app.get('/employees', (req, res) => {  res.json(employees);});app.listen(3000, () => console.log('server started'));


Вышеприведенный код просто ссылается на apicache при помощи apicache.middleware, в результате имеем:

app.use(cache('5 minutes'))

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

Версионирование API

У нас должны быть различные версии API на тот случай, если мы вносим в них такие изменения, которые могут нарушить работу клиента. Версионирование может производиться по семантическому принципу (например, 2.0.6 означает, что основная версия 2, и это шестой патч). Такой принцип сегодня принят в большинстве приложений.
Таким образом можно постепенно выводить из употребления старые конечные точки, а не вынуждать всех одновременно переходить на новый API. Можно сохранить версию v1 для тех, кто не хочет ничего менять, а версию v2 со всеми ее новоиспеченными возможностями предусмотреть для тех, кто готов обновиться. Это особенно важно в контексте публичных API. Их нужно версионировать, чтобы не сломать сторонние приложения, использующие наши API.
Версионирование обычно делается путем добавления /v1/, /v2/, т.д., добавляемых в начале пути к API.

Например, вот как это можно сделать в Express:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/v1/employees', (req, res) => {  const employees = [];  // код для получения информации о сотрудниках  res.json(employees);});app.get('/v2/employees', (req, res) => {  const employees = [];  // другой код для получения информации о сотрудниках  res.json(employees);});app.listen(3000, () => console.log('server started'));


Мы просто добавляем номер версии к началу пути, ведущего к конечной точке.

Заключение

Важнейший вывод, связанный с проектированием высококачественных REST API: в них необходимо сохранять единообразие, следуя стандартам и соглашениям, принятым в вебе. JSON, SSL/TLS и коды состояния HTTP обязательная программа в современном вебе.
Не менее важно учитывать производительность. Можно увеличить ее, не возвращая слишком много данных сразу. Кроме того, можно задействовать кэширование, чтобы не запрашивать одни и те же данные снова и снова.

Пути к конечным точкам должны именоваться единообразно. В их названиях нужно использовать существительные, поскольку глаголы присутствуют в названиях методов HTTP. Пути вложенных ресурсов должны следовать после пути родительского ресурса. Они должны сообщать, что мы получаем или чем манипулируем, так, чтобы нам не приходилось дополнительно обращаться к документации, чтобы понять, что происходит.
Подробнее..

Пишем full stack монолит с помощью Angular Universal NestJS PostgreSQL

10.08.2020 02:14:34 | Автор: admin
Привет, Хабр!

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


Эта статья будет полезна, если вы:


  • Начинающий fullstack-разработчик;
  • Стартапер, который пишет MVP чтобы проверить гипотезу.

Почему выбрал такой стек:


  • Angular: имею много опыта в нем, люблю строгую архитектуру и Typescript из коробки, выходец из .NET
  • NestJS: тот-же язык, та-же архитектура, быстрое написание REST API, возможность в дальнейшем пересесть на Serverless (дешевле виртуалки)
  • PostgreSQL: Собираюсь хоститься в Яндекс.Облаке, на минималках дешевле на 30% чем MongoDB

Прайс яндекса


Прежде чем написать статью, поискал на хабре статьи про подобный кейс, нашел следующее:



Из этого ничего не описывает "скопировал и вставил" или дает ссылки на то что еще нужно дорабатывать.


Оглавление:


1. Создаем Angular приложение и добавляем библиотеку компонентов ng-zorro
2. Устанавливаем NestJS и решаем проблемы с SSR
3. Делаем API на NestJS и подключаем к фронту
4. Подключаем базу данных PostgreSQL



1. Создаем Angular приложение


Установим Angular-CLI чтобы создавать SPA-сайты на Ангуляре:


npm install -g @angular/cli

Создадим Angular приложение с помощью следующей команды:


ng new angular-habr-nestjs

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


cd angular-habr-nestjsng serve --open

Статическое SPA-приложение на Angular


Приложение создалось. Подключаем библиотеку NG-Zorro:


ng add ng-zorro-antd

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


? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] No? Choose your locale code: ru_RU? Choose template to create project: sidemenu

Эта конфигурация заменит содержимое app.component на дизайн с менюшкой слева, футером и хедером и подключит локализацию на русском языке:


Подключили NG-Zorro


В данной статье мы отобразим список данных для наглядности, поэтому добавим простенькую табличку в компоненте src/app/pages/welcome, который сгенерил NG-Zorro:
Пример взят отсюда:
https://ng.ant.design/components/table/en


// welcome.component.html<nz-table #basicTable [nzData]="items$ | async"> <thead> <tr>  <th>Name</th>  <th>Age</th>  <th>Address</th> </tr> </thead> <tbody> <tr *ngFor="let data of basicTable.data">  <td>{{ data.name }}</td>  <td>{{ data.age }}</td>  <td>{{ data.address }}</td> </tr> </tbody></nz-table>

// welcome.module.tsimport { NgModule } from '@angular/core';import { WelcomeRoutingModule } from './welcome-routing.module';import { WelcomeComponent } from './welcome.component';import { NzTableModule } from 'ng-zorro-antd';import { CommonModule } from '@angular/common';@NgModule({ imports: [  WelcomeRoutingModule,  NzTableModule, // Добавили для таблицы  CommonModule // Добавили для пайпа async ], declarations: [WelcomeComponent], exports: [WelcomeComponent]})export class WelcomeModule {}

// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = of([  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]); constructor(private http: HttpClient) { } ngOnInit() { } // Сразу напишем метод к бэку, понадобится позже getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Получилось следующее:


Табличка NG-Zorro



2. Устанавливаем NestJS


Далее установим NestJS таким образом, чтобы он предоставил Angular Universal (Server Side Rendering) из коробки и напишем пару ендпоинтов.


ng add @nestjs/ng-universal

После установки, запускаем наш SSR с помощью команды:


npm run serve

И вот уже первый косяк :) У нас появляется следующая ошибка:


TypeError: Cannot read property 'indexOf' of undefined  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:35:43  at D:\Projects\angular-habr-nestjs\dist\server\main.js:107572:13  at View.engine (D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:30:11)  at View.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\view.js:135:8)  at tryRender (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:640:10)  at Function.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:592:3)  at ServerResponse.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\response.js:1012:7)  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\angular-universal.module.js:60:66  at Layer.handle [as handle_request] (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\layer.js:95:5)  at next (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\route.js:137:13)

Чтобы решить косяк, зайдем в файл server/app.module.ts и поменяем значение liveReload на false:


import { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }) ]})export class ApplicationModule {}

Также подтюним конфиг тайпскрипта, так-как эта конфигурация не взлетает с использованием Ivy рендера:


// tsconfig.server.json{ "extends": "./tsconfig.app.json", "compilerOptions": {  "outDir": "./out-tsc/server",  "target": "es2016",  "types": [   "node"  ] }, "files": [  "src/main.server.ts" ], "angularCompilerOptions": {  "enableIvy": false, // Добавили флажок  "entryModule": "./src/app/app.server.module#AppServerModule" }}

После пересоберем приложение командой ng run serve чтобы SSR заработал.


Angular SSR + NestJS


Ура! SSR подрубился, но как видимо в devtools он приходит с кривыми стилями.


Добавим extractCss: true, который позволит выносить стили не в styles.js, а в styles.css:


// angular.json..."architect": {    "build": {     "builder": "@angular-devkit/build-angular:browser",     "options": {      "outputPath": "dist/browser",      "index": "src/index.html",      "main": "src/main.ts",      "polyfills": "src/polyfills.ts",      "tsConfig": "tsconfig.app.json",      "aot": true,      "assets": [       "src/favicon.ico",       "src/assets",       {        "glob": "**/*",        "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",        "output": "/assets/"       }      ],      "extractCss": true, // Добавили флажок      "styles": [       "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",       "src/styles.scss"      ],      "scripts": []     },...

Также подключим стили библиотеки в app.component.scss:


// app.component.scss@import "~ng-zorro-antd/ng-zorro-antd.min.css"; // Подключили стили:host { display: flex; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.app-layout { height: 100vh;}...

Теперь стили подключены, SSR отдает страничку со стилями, но мы видим что сначала у нас грузится SSR, потом страница моргает и отрисовывается CSR (Client Side Rendering). Это решается следующим способом:


import { NgModule } from '@angular/core';import { Routes, RouterModule } from '@angular/router';const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: '/welcome' }, { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) }];@NgModule({ imports: [RouterModule.forRoot(routes, {initialNavigation: 'enabled', scrollPositionRestoration: 'enabled'})], // Добавили initialNavigation, scrollPositionRestoration exports: [RouterModule]})export class AppRoutingModule { }

  • initialNavigation: 'enabled' дает инструкцию роутингу не отрисовывать страницу, если уже загружена через SSR
  • scrollPositionRestoration: 'enabled' скролит страницу наверх при каждом роутинге.


3. Сделаем пару ендпоинтов на NestJS


Перейдем в папку server и создадим первый контроллер items:


cd servernest g module itemsnest g controller items --no-spec

// items.module.tsimport { Module } from '@nestjs/common';import { ItemsController } from './items.controller';@Module({ controllers: [ItemsController]})export class ItemsModule {}

// items.controller.tsimport { Controller } from '@nestjs/common';@Controller('items')export class ItemsController {}

Контроллер и модуль создались. Создадим метод на получение списка items и на добавление объекта в список:


// server/src/items/items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';class Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { // для простоты данные взял из Angular private items: Item[] = [  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]; @Get() getAll(): Item[] {  return this.items; } @Post() create(@Body() newItem: Item): void {  this.items.push(newItem); }}

Попробуем вызвать GET в Postman:


GET запросы апишки NestJS


Отлично, работает! Обратите внимание, вызываем метод GET items с префиксом api, который ставится автоматически в файле server/main.ts при установке NestJS:


// server/main.tsimport { NestFactory } from '@nestjs/core';import { ApplicationModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(ApplicationModule); app.setGlobalPrefix('api'); // Это префикс await app.listen(4200);}bootstrap();

Теперь прикрутим бэк к фронту. Возвращаемся к файлу welcome.component.ts и делаем запрос списка к бэку:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Можно увидеть что апиха на фронте дергается, но также дергается и в SSR, причем с ошибкой:


Дергание апихи в SSR


Ошибка при запросе в SSR решается следующим способом:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('http://localhost:4200/api/items').pipe(share()); // Прописали полный путь к апихе чтобы SSR не ругался }}interface Item { name: string; age: number; address: string;}

Чтобы исключить двойной запрос к апихе (один на SSR, другой на фронте), нужно проделать следующее:


  • Установим библиотеку @nguniversal/common:

npm i @nguniversal/common

  • В файле app/app.module.ts добавим модуль для запросов из SSR:

// app.module.tsimport { BrowserModule } from '@angular/platform-browser';import { NgModule } from '@angular/core';import { AppRoutingModule } from './app-routing.module';import { AppComponent } from './app.component';import { IconsProviderModule } from './icons-provider.module';import { NzLayoutModule } from 'ng-zorro-antd/layout';import { NzMenuModule } from 'ng-zorro-antd/menu';import { FormsModule } from '@angular/forms';import { HttpClientModule } from '@angular/common/http';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';import { NZ_I18N } from 'ng-zorro-antd/i18n';import { ru_RU } from 'ng-zorro-antd/i18n';import { registerLocaleData } from '@angular/common';import ru from '@angular/common/locales/ru';import {TransferHttpCacheModule} from '@nguniversal/common';registerLocaleData(ru);@NgModule({ declarations: [  AppComponent ], imports: [  BrowserModule.withServerTransition({ appId: 'serverApp' }),  TransferHttpCacheModule, // Добавили  AppRoutingModule,  IconsProviderModule,  NzLayoutModule,  NzMenuModule,  FormsModule,  HttpClientModule,  BrowserAnimationsModule ], providers: [{ provide: NZ_I18N, useValue: ru_RU }], bootstrap: [AppComponent]})export class AppModule { }

Схожую операцию проделаем с app.server.module.ts:


// app.server.module.tsimport { NgModule } from '@angular/core';import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';import { AppModule } from './app.module';import { AppComponent } from './app.component';@NgModule({ imports: [  AppModule,  ServerModule,  ServerTransferStateModule, // Добавили ], bootstrap: [AppComponent],})export class AppServerModule {}

Хорошо. Теперь получаем данные из апи в SSR, отрисовываем на форме, отдаем на фронт и тот не делает повторных запросов.


Запроса нет, данные есть!



4. Подключим базу PostgreSQL


Подключим библиотеки для работы с PostgreSQL, также будем использовать TypeORM для работы с базой:


npm i pg typeorm @nestjs/typeorm

Внимание: у вас уже должна быть установлена PostgreSQL с базой внутри.


Описываем конфиг подключения к базе в server/app.module.ts:


// server/app.module.tsimport { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';import { ItemsController } from './src/items/items.controller';import { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }),  TypeOrmModule.forRoot({ // Конфиг подключения к базе   type: 'postgres',   host: 'localhost',   port: 5432,   username: 'postgres',   password: 'admin',   database: 'postgres',   entities: ['dist/**/*.entity{.ts,.js}'],   synchronize: true  }) ], controllers: [ItemsController]})export class ApplicationModule {}

Немного про поля конфига:


  • type: указываем название типа базы данных, к которой подключаемся
  • host и port: место где база хостится
  • username и password: аккаунт для этой базы
  • database: название базы
  • entities: путь, откуда будем брать сущности для схемы нашей базы

По последнему пункту, нужно создать сущность Item для мапинга полей в базу:


// server/src/items/item.entity.tsimport { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm/index';@Entity()export class ItemEntity { @PrimaryGeneratedColumn() id: number; @CreateDateColumn() createDate: string; @Column() name: string; @Column() age: number; @Column() address: string;}

Далее свяжем эту сущность с нашей базой.


// items.module.tsimport { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { ItemEntity } from './item.entity';import { ItemsController } from './items.controller';@Module({ imports: [  TypeOrmModule.forFeature([ItemEntity]) // Подключаем фича-модуль и указываем сущности базы ], controllers: [ItemsController]})export class ItemsModule {}

Теперь укажем в контроллере, что хотим работать с базой, а не кешем:


// items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';import { ItemEntity } from './item.entity';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm/index';interface Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { constructor(@InjectRepository(ItemEntity)       private readonly itemsRepository: Repository<ItemEntity>) { // Подключили репозиторий } @Get() getAll(): Promise<Item[]> {  return this.itemsRepository.find(); } @Post() create(@Body() newItem: Item): Promise<Item> {  const item = this.itemsRepository.create(newItem);  return this.itemsRepository.save(item); }}

Проверим работу апихи в Postman:


POST к апихе с базой


Работает. Потыкали несколько раз постман, посмотрим что записалось в базе с помощью DBeaver:


Записи в базе


Отлично! В базе есть, посмотрим как выглядит на фронте:


Рабочее fullstack приложение


Готово! Мы сделали fullstack приложение, с которым можно работать дальше.


P.S. Сразу поясню следующее:


  • Вместо Ng-Zorro вы можете использовать любую другую библиотеку, например Angular Material. Мне она лично не зашла из-за сложности разработки;
  • Я знаю, что нужно на бэке использовать сервисы, а не напрямую дергать базу в контроллерах. Эта статья о том, как решив проблемы "влоб" получить MVP с которым можно работать, а не про архитектуру и паттерны;
  • Вместо вписывания на фронте http://localhost:4200/api возможно лучше написать интерсептор и проверять откуда мы стучимся

Полезные ссылки:


Подробнее..

Перевод Руководство по Express.js. Часть 1

27.08.2020 12:18:32 | Автор: admin


Доброго времени суток, друзья!

Представляю вашему вниманию перевод первой части Руководства по Express веб-феймворку для Node.js автора Flavio Copes.

Предполагается, что вы знакомы с Node.js. Если нет, то прошу сюда.

Без дальнейших предисловий.

1. Введение


Express это веб-фреймворк для Node.js.

Node.js замечательный инструмент для создания сетевых сервисов и приложений.

Express использует возможности Node.js, значительно облегчая процесс создания веб-сервера.

Он является открытым, бесплатным, расширяемым и предоставляет множество готовых решений для создания сервера.

2. Установка


Express можно установить с помощью npm:

npm i express

Или yarn:

yarn add express

Для инициализации нового проекта выполните команду npm init или yarn init. Для автоматического заполнения полей следует добавить флаг -y.

3. Hello World


Мы готовы к созданию нашего первого сервера.

Вот его код:

const express = require('express')const app = express()app.get('/', (req, res) => res.send('Hello World!'))app.listen(3000, () => console.log('Сервер запущен'))

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

node index.js

Откройте браузер на localhost:3000 и увидете сообщения Hello World! на экране и Сервер запущен в консоли.

4. Основы Express


Эти 4 строки кода делают множество вещей за кулисами.

Сначала мы импортируем библиотеку express.

Затем инициализируем приложение, вызывая метод app().

После получения объекта приложения мы указываем ему обрабатывать GET-запросы к пути "/" с помощью метод get().

Для каждого HTTP-метода или, как еще говорят, глагола (хотя среди методов встречаются и существительные) имеется соответствующий метод Express:

app.get('/', (req, res) => {})app.post('/', (req, res) => {})app.put('/', (req, res) => {})app.delete('/', (req, res) => {})app.patch('/', (req, res) => {})

Все эти методы принимают колбек, вызываемый при получении запроса для его обработки.

Мы передаем его так:

(req, res) => res.send('Hello World!')

Аргументы req и res соответствуют объектам Request (запрос) и Response (ответ).

Request содержит информацию о запросе, включая параметры, заголовки, тело запроса и т.д.

Response это объект, отправляемый клиенту в ответ на запрос.

В нашем коде мы отправляем клиенту строку Hello World! с помощью метода Response.send().

Данный метод помещает строку в тело ответа и закрывает соединение.

Последняя строка кода запускает сервер и указывает ему слушать порт 3000. Передаваемый колбек вызывается при готовности сервера к получению запросов.

6. Параметры запроса


Объект Request содержит информацию о запросе.

Ниже приведены основные свойства этого объекта.
Свойство Описание
.app содержит ссылку на объект приложения
.baseUrl содержит ссылку на экземпляр маршрутизатора (express.Router())
.body содержит данные, помещенные в тело запроса (должны быть разобраны (parsed) и заполнены (populated) перед использованием)
.cookies содержит куки, установленные в запросе (требуется промежуточное программное обеспечение (далее ППО) cookie-parser)
.hostname название хоста сервера
.ip IP-адрес сервера
.method метод запроса
.params объект с именованными параметрами запроса (например, при запросе к /users/:id, id будет записано в req.params.id)
.path URL запроса
.protocol протокол запроса
.query объект с параметрами строки запроса (например, при запросе к /search?name=john, john будет записано в req.query.name)
.secure содержит true, если запрос является безопасным (если используется HTTPS)
.signedCookies содержит подписанные куки (требуется ППО cookie-parser)
.xhr содержит true, если запрос это XMLHttpRequest

7. Получение параметров из строки запроса


Строка запроса это часть URL после вопросительного знака (?).

Например:

?name=john

Несколько параметров могут передаваться с помощью амперсанда (&):

?name=john&age=30

Как извлечь эти значения?

Это делается посредством распаковывания объекта Request.query:

const express = require('express')const app = express()app.get('/', (req, res) => {    console.log(req.query)})app.listen(8080)

Данный объект содержит свойство для каждого параметра строки запроса.

Если параметры отсутствуют, объект является пустым.

Перебрать объект можно с помощью цикла for/in:

for (const key in req.query) {    console.log(key, req.query[key])}

Этот код выведет в консоль ключи и значения параметров строки запроса.

Также можно получить значение конкретного параметра:

req.query.name // johnreq.query.age // 30

8. Получение параметров строки POST-запроса


Параметры строки POST-запроса предоставляются клиентом при отправке формы или других данных.

Как нам получить эти параметры?

Если данные были отправлены в формате JSON с помощью Content-Type: application/json, такие данные необходимо разобрать с помощью ППО express.json(). ППО подключается с помощью метода app.use():

const express = require('express')const app = express()app.use(express.json())

Если данные были отправлены в формате JSON с помощью Content-Type: application/x-www-urlencoded, такие данные необходимо разобрать с помощью ППО express.urlencoded():

const express = require('express')const app = express()app.use(express.urlencoded())

В обоих случаях данные можно получить через Request.body:

app.post('/form', (req, res) => {    const name = req.body.name})

Обратите внимание, что в старых версиях Express для обработки данных в качестве ППО использовался модуль body-parcer. В настоящее время данный модуль встроен в Express.

9. Отправка ответа


Как отправить ответ клиенту?


В приведенном примере мы использовали метод Response.send() для отправки клиенту ответа в виде строки и закрытия соединения:

(req, res) => res.send('Hello World!')

При передачи строки, заголовок Content-Type устанавливается в значение text/html.

При передачи объекта или массива, заголовок Content-Type устанавливается в значение application/json, а данные преобразуются в формат JSON.

send() также автоматически устанавливает заголовок Content-Length и закрывает соединение с сервером.

Использование end() для отправки пустого ответа


Альтернативным способом отправки клиенту ответа, не содержащего тела, является использование метода Response.end():

res.end()

Установка статуса ответа


Для этого используется метод Response.status():

res.status(404).end()

Или:

res.status(404).send('Файл не найден')

sendStatus() является сокращением для res.status().send():

res.sendStatus(200) // === res.status(200).send('Ok')res.sendStatus(403) // === res.status(403).send('Forbidden')res.sendStatus(404) // === res.status(404).send('Not Found')res.sendStatus(500) // === res.status(500).send('Internal Server Error')

10. Отправка ответа в формате JSON


При обработке запросов маршрутизатором колбек вызывается с двумя параметрами экземпляром объекта Request и экземпляром объекта Response.

Например:

app.get('/', (req, res) => res.send('Hello World!'))

Здесь мы используем метод Response.send(), принимающий строку.

Ответ клиенту в формате JSON можно отправить с помощью метода Response.json().

Данный метод принимате объект или массив и конвертирует его в JSON:

res.json({ name: 'John' }) // { "name": "John" }

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

Следите за обновлениями. Благодарю за внимание и хорошего дня.
Подробнее..

Перевод Руководство по Express.js. Часть 3

12.09.2020 16:16:00 | Автор: admin


Доброго времени суток, друзья!

Представляю вашему вниманию перевод второй части Руководства по Express веб-феймворку для Node.js автора Flavio Copes.

Предполагается, что вы знакомы с Node.js. Если нет, то прошу сюда.

Без дальнейших предисловий.

15. Шаблонизация


Express умеет работать с серверными шаблонизаторами (server-side template engines).

Шаблонизатор позволяет динамически генерировать HTML-разметку посредством передачи данных в представление (view).

По умолчанию Express использует шаблонизатор Pug, который раньше назывался Jade.

Несмотря на то, что с момента выхода последней версии Jade прошло почти 3 года, он все еще поддерживается в целях обеспечения обратной совместимости.

В новых проектах следует использовать Pug или другой шаблонизатор. Вот официальный сайт Pug: pugjs.org.

Среди других шаблонизаторов, можно назвать Handlebars, Mustache, EJS и др.

Использование Pug


Для использования Pug, его сначала нужно установить:

npm i pug 

Затем его следует добавить в Express:

const express = require('express')const app = express()app.set('view engine', 'pug')

После этого мы можем создавать шаблоны в файлах с расширением .pug.

Создадим представление about:

app.get('/about', (req, res) => {    res.render('about')})

И шаблон в views/about.pug:

p Привет от Pug 

Данный шаблон создаст тег p с текстом Привет от Pug.

Интерполировать переменные можно так:

app.get('/about', (req, res) => {    res.render('about', { name: 'Иван' })})

p #{name} говорит привет

Подробнее о Pug мы поговорим в следующем разделе.

При использовании шаблонизатора для динамической генерации страниц можно столкнуться с некоторыми проблемами, особенно, когда возникает необходимость преобразовать HTML, например, в Pug. Для этого в сети существуют готовые решения. Вот одно из них: html-to-pug.com.

Использование Handlebars


Попробуем использовать Handlebars вместо Pug.

Устанавливаем его с помощью npm i handlebars.

В директории views создаем файл about.hbs следующего содержания:

{{name}} говорит привет 

Перепишем код Express:

const express = require('express')const app = express()const hbs = require('hbs')app.set('view engine', 'hbs')app.set('views', path.join(__dirname, 'views'))app.get('/about', (req, res) => {    res.render('about', { name: 'Иван' })})app.listen(3000, () => console.log('Сервер готов'))

Вы также можете рендерить React на стороне сервера с помощью пакета express-react-views.

Устанавливаем данный пакет:

npm i express-react-views

Теперь вместо Handlebars укажем Express использовать express-react-views в качестве движка для обработки jsx-файлов:

const express = require('express')const app = express()app.set('view engine', 'jsx')app.engine('jsx', require('express-react-views').createEngine())app.get('/about', (req, res) => {    res.render('about', { name: 'Иван' })})app.listen(3000, () => console.log('Сервер запущен'))

В директории views создаем файл about.jsx:

const React = require('react')class HelloMessage extends React.Component {    render() {        return <div>{this.props.name} говорит привет</div>    }}module.exports = HelloMessage

16. Справочник по Pug


Что такое Pug? Это шаблонизатор или движок для динамического рендеринга HTML-разметки, используемый Express по умолчанию.

Установка:

npm i pug

Настройка Express:

const path = require('path')cpnst express = require('express')const app = express()app.set('view engine', 'pug')app.set('views', path.join(__dirname, 'views'))app.get('/about', (req, res) => {    res.render('about', { name: 'Иван' })})

Шаблон (about.pug):

p #{name} говорит привет 

Передача функции, возвращающей значение:

app.get('about', (req, res) => {    res.render('about', { getName: () => 'Иван' })})

p #{getName()} говорит привет 

Добавление элементу идентификатора или класса:

p#title p.title 
Установка doctype:

doctype html 

Мета-теги:

html     head         meta(charset='utf-8')        meta(http-equiv='X-UA-Compatible', content='IE=edge')        meta(name='description', content='Описание')        meta(name='viewport', content='width=device-width, initial-scale=1')

Добавление скриптов или стилей:

html     head         link(rel="stylesheet", href="style.css")        script(src="script.js", defer)

Встроенные скрипты:

    script alert('тест')        script        (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]= function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date; e=o.createElement(i);r=o.getElementsByTagName(i)[0]; e.src='//www.google-analytics.com/analytics.js'; r.parentNode.insertBefore(e,r)}(window,document,'script','ga')); ga('create','UA-XXXXX-X');ga('send','pageview');

Циклы:

ul     each color in ['Red', 'Green', 'Blue']        li= colorul     each color, index in ['Red', 'Green', 'Blue']        li= 'Номер цвета ' + index + ':' + color

Условия:

if name     h2 #{name} говорит привет else    h2 Привет if name    h2 #{name} говорит привет else if anotherName    h2 #{anotherName} говорит привет else    h2 Привет 

Переменные:

- var name = 'Иван'- age = 30 - var petr = { name: 'Петр' }- var friends = ['Иван', 'Петр']

Инкремент:

age++

Приваивание переменной элементу:

p= name span= age 

Перебор переменных:

for friend in friends     li= friendul     each friend in friends         li= friend 
Получение количества элементов:

p #{values.length}

While:

- var n = 0ul     while n <= 5        li= n++

Включение одного файла в другой:

include otherfile.pug 

Определение блоков.

Хорошо организованная система шаблонов предполагает создание базового шаблона и его расширение другими шаблонами по мере необходимости.

Базовый шаблон расширяется с помощью блоков:

html    head        link(rel="stylesheet", href="style.css")        script(src="script.js", defer)        body            block header                        block main                h1 Домашняя страница                p Добро пожаловать                        block footer

Расширение базового шаблона.

Шаблон расширяется с помощью ключевого слова extends:

extends home.pug 

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

extends home.pug block main     h1 Другая страница    p Привет     ul        li Раз элемент списка        li Два элемент списка 

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

Комментарии.

Видимые (сохраняются в разметке):

// однострочный комментарий//    многострочный    комментарий

Невидимые (удаляются при рендеринге):

//- однострочный комментарий//-     многострочный    комментарий

17. Middleware (промежуточный слой, промежуточное программное обеспечение)


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

Обычно, middleware применяется для обработки запроса/ответа либо для перехвата запроса перед его обработкой роутером. Middleware в общем виде выглядит так:

app.use((req, res, next) => {/* */})

Метод next() служит для передачи запроса следующему middleware, если в этом есть необходимость.

Если опустить next() в middleware, то обработка ответа завершится и он будет отправлен клиенту.

Middleware редко пишется вручную, как правило, в качестве таковых используются npm-пакеты.

Примером подобного пакета является cookie-parser, применяемый для преобразования куки в объект req.cookies. Данный пакет устанавливается с помощью npm i cookie-parser и используется следующим образом:

const express = require('express')const app = express()const cookieParser = require('cookie-parser')app.get('/', (req, res) => res.send('Привет!'))app.use(cookieParser())app.listen(3000, () => console.log('Сервер готов'))

Middleware может использоваться в определенном маршрутизаторе. Для этого он передается роутеру в качестве второго аргумента:

const myMiddleware = (req, res, next) => {    // ...     next()}app.get('/', myMiddleware, (req, res) => res.send('Привет!'))

Для того, чтобы сохранить данные, сформированные middleware, для их передачи другим middleware или обработчику запроса, используется объект Request.locals. Он позволяет записывать данные в текущий запрос:

req.locals.name = 'Иван'

18. Обработка статических файлов


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

const express = require('express')const app = express()app.use(express.static('public'))// ... app.listen(3000, () => console.log('Сервер готов'))

Если в директории public имеется файл index.html, он будет отправлен в ответ на запрос к localhost:3000.

19. Отправка файлов


Express предоставляет удобный метод для отправки файлов в ответ на запрос Reaponse.download().

После обращения клиента к маршруту, отправляющему в ответ файл, клиенту будет направлен запрос на скачивание файла.

Метод Response.download() позволяет отправлять файлы в ответ на запрос вместо отображения страницы:

app.get('/', (req, res) => res.download('./file.pdf'))

В контексте приложения это выглядит так:

const express = require('express')const app = express()app.get('/', (req, res) => res.download('./file.pdf'))app.listen(3000, () => console.log('Сервер готов'))

При отправке файла можно определить его название:

res.download('./file.pdf', 'some-custom-name.pdf')

Третим параметром рассматриваемого метода является колбэк, вызываемый после отправки файла:

res.download('./file.pdf', 'some-custom-name.pdf', error => {    if (error) {        // обрабатываем ошибку    } else {        console.log('Файл успешно отправлен')    }})

20. Сессии


По умолчанию запросы в Express являются одноразовыми, т.е. запросы не могут связываться между собой. Не существует способа определить, что повторный запрос отправлен тем же клиентом.

Пользователи не могут быть идентифицированы нативными средствами.

Вот где в игру вступают сессии.

После реализации названного механизма, каждому пользователю и сайту присваивается сессия, хранящая пользовательское состояние.

Мы будем использовать express-session, поддерживаемый командой Express.

Устанавливаем его:

npm i express-session

Инициализируем:

const session = require('express-session')

Добавляем в Express в качестве middleware:

const express = require('express')const session = require('express-session')const app = express()app.use(session(    'secret': '343ji43j4n3jn4jk3n'))

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

Единственным обязательным параметром является secret, но существуют и другие. secret произвольная случайная строка, уникальная для приложения.

Сессия включается в состав запроса, доступ к ней можно получить через req.session:

app.get('/', (req, res, next) => {    // req.session })

Данный объект может быть использован как для получения данных, так и для их записи:

req.sessions.name = 'Иван'console.log(req.sessions.name) // Иван 

При записи данные сериализуются (преобразуются в формат JSON), так что можно смело использовать вложенные объекты.

Сессии используются для передачи данных другим middleware или для их извлечения при последующих запросах.

Где хранятся сессионные данные? Это зависит от того, как настроен модуль express-session.

Такие данные могут храниться в:

  • памяти только при разработке
  • базе данных, например, MySQL или Mongo
  • кэше, например, Redis или Memcached

В любом случае идентификатор сессии записывается в куки, а сами данные хранятся на сервере. Куки с идентификатором сессии включается в каждый запрос клиента.

По идентификатору сервер осуществляет поиск хранящихся на нем данных.

По умолчанию для хранения данных используется память, но это подходит только для разработки.

Лучшим решением является Redis, однако, она требует дополнительной настройки инфраструктуры.

Другим популярным решением является пакет cookie-session. Он сохраняет данные в куки на стороне клиента. Данный способ использовать не рекомендуется, поскольку данные будут включаться в каждый запрос клиента и размер данных ограничен 4 Кб. Кроме того, обычные куки не подходят для хранения конфиденциальной информации. Существуют безопасные куки, передаваемые по протоколу HTTPS, но такие куки требуют дополнительной настройки с помощью прокси-сервера.

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

Следите за обновлениями. Благодарю за внимание и хороших выходных.
Подробнее..

Полный стек на примере списка задач (React, Vue, TypeScript, Express, Mongoose)

24.12.2020 12:19:48 | Автор: admin


Доброго времени суток, друзья!

В данном туториале я покажу вам, как создать фуллстек-тудушку.

Наше приложение будет иметь стандартный функционал:

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

Выглядеть наше приложение будет так:


Для более широкого охвата аудитории клиентская часть приложения будет реализована на чистом JavaScript, серверная на Node.js. В качестве абстракции для ноды будет использован Express.js, в качестве базы данных сначала локальное хранилище (Local Storage), затем индексированная база данных (IndexedDB) и, наконец, облачная MongoDB.

При разработке клиентской части будут использованы лучшие практики, предлагаемые такими фреймворками, как React и Vue: разделение кода на автономные переиспользуемые компоненты, повторный рендеринг только тех частей приложения, которые подверглись изменениям и т.д. При этом, необходимый функционал будет реализован настолько просто, насколько это возможно. Мы также воздержимся от смешивания HTML, CSS и JavaScript.

В статье будут приведены примеры реализации клиентской части на React и Vue, а также фуллстек-тудушки на React + TypeScript + Express + Mongoose.

Исходный код всех рассматриваемых в статье проектов находится здесь.

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

Демо нашего приложения:


Итак, поехали.

Клиент


Начнем с клиентской части.

Создаем рабочую директорию, например, javascript-express-mongoose:

mkdir javascript-express-mongoosecd !$code .

Создаем директорию client. В этой директории будет храниться весь клиентский код приложения, за исключением index.html. Создаем следующие папки и файлы:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    router.js    storage.js  script.js  style.css

В корне проекта создаем index.html следующего содержания:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>JS Todos App</title>    <!-- Подключаем стили -->    <link rel="stylesheet" href="client/style.css" />  </head>  <body>    <div id="root"></div>    <!-- Подключаем скрипт -->    <script src="client/script.js" type="module"></script>  </body></html>

Стили (client/style.css):
@import url('https://fonts.googleapis.com/css2?family=Stylish&display=swap');* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: stylish;  font-size: 1rem;  color: #222;}#root {  max-width: 512px;  margin: auto;  text-align: center;}#title {  font-size: 2.25rem;  margin: 0.75rem;}#counter {  font-size: 1.5rem;  margin-bottom: 0.5rem;}#form {  display: flex;  margin-bottom: 0.25rem;}#input {  flex-grow: 1;  border: none;  border-radius: 4px;  box-shadow: 0 0 1px inset #222;  text-align: center;  font-size: 1.15rem;  margin: 0.5rem 0.25rem;}#input:focus {  outline-color: #5bc0de;}.btn {  border: none;  outline: none;  background: #337ab7;  padding: 0.5rem 1rem;  border-radius: 4px;  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);  color: #eee;  margin: 0.5rem 0.25rem;  cursor: pointer;  user-select: none;  width: 102px;  text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);}.btn:active {  box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;}.btn.info {  background: #5bc0de;}.btn.success {  background: #5cb85c;}.btn.warning {  background: #f0ad4e;}.btn.danger {  background: #d9534f;}.btn.filter {  background: none;  color: #222;  text-shadow: none;  border: 1px dashed #222;  box-shadow: none;}.btn.filter.checked {  border: 1px solid #222;}#list {  list-style: none;}.item {  display: flex;  flex-wrap: wrap;  justify-content: space-between;  align-items: center;}.item + .item {  border-top: 1px dashed rgba(0, 0, 0, 0.5);}.text {  flex: 1;  font-size: 1.15rem;  margin: 0.5rem;  padding: 0.5rem;  background: #eee;  border-radius: 4px;}.completed .text {  text-decoration: line-through;  color: #888;}.disabled {  opacity: 0.8;  position: relative;  z-index: -1;}#modal {  position: absolute;  top: 10px;  left: 10px;  padding: 0.5em 1em;  background: rgba(0, 0, 0, 0.5);  border-radius: 4px;  font-size: 1.2em;  color: #eee;}


Наше приложение будет состоять из следующих частей (основные компоненты выделены зеленым, дополнительные элементы синим):


Основные компоненты: 1) форма, включающая поле для ввода текста задачи и кнопку для добавления задачи в список; 2) контейнер с кнопками для фильтрации задач; 3) список задач. Также в качестве основного компонента мы дополнительно выделим элемент списка для обеспечения возможности рендеринга отдельных частей приложения.

Дополнительные элементы: 1) заголовок; 2) счетчик количества невыполненных задач.

Приступаем к созданию компонентов (сверху вниз). Компоненты Form и Buttons являются статическими, а List и Item динамическими. В целях дифференциации статические компоненты экспортируются/импортируются по умолчанию, а в отношении динамических компонентов применяется именованный экспорт/импорт.

client/Form.js:

export default /*html*/ `<div id="form">  <input      type="text"      autocomplete="off"      autofocus      id="input"  >  <button    class="btn"    data-btn="add"  >    Add  </button></div>`

/*html*/ обеспечивает подсветку синтаксиса, предоставляемую расширением для VSCode es6-string-html. Атрибут data-btn позволит идентифицировать кнопку в скрипте.

Обратите внимание, что глобальные атрибуты id позволяют обращаться к DOM-элементам напрямую. Дело в том, что такие элементы (с идентификаторами), при разборе и отрисовке документа становятся глобальными переменными (свойствами глобального объекта window). Разумеется, значения идентификаторов должны быть уникальными для документа.

client/Buttons.js:

export default /*html*/ `<div id="buttons">  <button    class="btn filter checked"    data-btn="all"  >    All  </button>  <button    class="btn filter"    data-btn="active"  >    Active  </button>  <button    class="btn filter"    data-btn="completed"  >    Completed  </button></div>`

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

client/Item.js (самый сложный компонент с точки зрения структуры):

/** * функция принимает на вход задачу, * которая представляет собой объект, * включающий идентификатор, текст и индикатор выполнения * * индикатор выполнения управляет дополнительными классами * и текстом кнопки завершения задачи * * текст завершенной задачи должен быть перечеркнут, * а кнопка для изменения (обновления) текста такой задачи - отключена * * завершенную задачу можно сделать активной*/export const Item = ({ id, text, done }) => /*html*/ `<li  class="item ${done ? 'completed' : ''}"  data-id="${id}">  <button    class="btn ${done ? 'warning' : 'success'}"    data-btn="complete"  >    ${done ? 'Cancel' : 'Complete'}  </button>  <span class="text">    ${text}  </span>  <button    class="btn info ${done ? 'disabled' : ''}"    data-btn="update"  >    Update  </button>  <button    class="btn danger"    data-btn="delete"  >    Delete  </button></li>`

client/List.js:

/** * для формирования списка используется компонент Item * * функция принимает на вход список задач * * если вам не очень понятен принцип работы reduce * https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce*/import { Item } from "./Item.js"export const List = (todos) => /*html*/ `  <ul id="list">    ${todos.reduce(      (html, todo) =>        (html += `            ${Item(todo)}        `),      ''    )}  </ul>`

С компонентами закончили.

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

src/helpers.js:

/** * данная функция будет использоваться * для визуализации нажатия одной из кнопок * для фильтрации задач * * она принимает элемент - нажатую кнопку и класс - в нашем случае checked * * основной контейнер имеет идентификатор root, * поэтому мы можем обращаться к нему напрямую * из любой части кода, в том числе, из модулей*/export const toggleClass = (element, className) => {  root.querySelector(`.${className}`).classList.remove(className)  element.classList.add(className)}// примерные задачиexport const todosExample = [  {    id: '1',    text: 'Learn HTML',    done: true  },  {    id: '2',    text: 'Learn CSS',    done: true  },  {    id: '3',    text: 'Learn JavaScript',    done: false  },  {    id: '4',    text: 'Stay Alive',    done: false  }]

Создадим базу данных (пока в форме локального хранилища).

src/storage.js:

/** * база данных имеет два метода * get - для получения тудушек * set - для записи (сохранения) тудушек*/export default (() => ({  get: () => JSON.parse(localStorage.getItem('todos')),  set: (todos) => { localStorage.setItem('todos', JSON.stringify(todos)) }}))()

Побаловались и хватит. Приступаем к делу.

src/script.js:

// импортируем компоненты, вспомогательную функцию, примерные задачи и хранилищеimport Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, todosExample } from './src/helpers.js'import storage from './src/storage.js'// функция принимает контейнер и список задачconst App = (root, todos) => {  // формируем разметку с помощью компонентов и дополнительных элементов  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  // обновляем счетчик  updateCounter()  // получаем кнопку добавления задачи в список  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал приложения  // функция добавления задачи в список  function addTodo() {    if (!input.value.trim()) return    const todo = {      // такой способ генерации идентификатора гарантирует его уникальность и соответствие спецификации      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // очищаем поле и устанавливаем фокус    clearInput()    updateCounter()  }  // функция завершения задачи  // принимает DOM-элемент списка  function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    // рендерим только изменившийся элемент    renderItem(item, todo)    updateCounter()  }  // функция обновления задачи  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    // тонкий момент: мы используем одну и ту же кнопку    // для добавления задачи в список и обновления текста задачи    $addBtn.textContent = 'Update'    // добавляем разовый обработчик    $addBtn.addEventListener(      'click',      (e) => {        // останавливаем распространение события для того,        // чтобы нажатие кнопки не вызвало функцию добавления задачи в список        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  // функция удаления задачи  function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    updateCounter()  }  // функция поиска задачи  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дополнительный функционал  // функция фильтрации задач  // принимает значение кнопки  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      // отобразить все задачи      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      // активные задачи      case 'active':        // отобразить все и отключить завершенные        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      // завершенные задачи      case 'completed':        // отобразить все и отключить активные        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  // функция обновления счетчика  function updateCounter() {    // считаем количество невыполненных задач    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  // функция повторного рендеринга изменившегося элемента  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  // функция очистки инпута  function clearInput() {    input.value = ''    input.focus()  }  // делегируем обработку событий корневому узлу  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  // обрабатываем нажатие Enter  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }  // оптимизация работы с хранилищем  window.onbeforeunload = () => {    storage.set(todos)  }}// инициализируем приложения;(() => {  // получаем задачи из хранилища  let todos = storage.get('todos')  // если в хранилище пусто  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

В принципе, на данном этапе мы имеем вполне работоспособное приложение, позволяющее добавлять, редактировать и удалять задачи из списка. Задачи записываются в локальное хранилище, так что сохранности данных ничего не угрожает (вроде бы).

Однако, с использованием локального хранилища в качестве базы данных сопряжено несколько проблем: 1) ограниченный размер около 5 Мб, зависит от браузера; 2) потенциальная возможность потери данных при очистке хранилищ браузера, например, при очистке истории просмотра страниц, нажатии кнопки Clear site data вкладки Application Chrome DevTools и т.д.; 3) привязка к браузеру невозможность использовать приложение на нескольких устройствах.

Первую проблему (ограниченность размера хранилища) можно решить с помощью IndexedDB.

Индексированная база данных имеет довольно сложный интерфейс, поэтому воспользуемся абстракцией Jake Archibald idb-keyval. Копируем этот код и записываем его в файл src/idb.js.

Вносим в src/script.js следующие изменения:

// import storage from './src/storage.js'import { get, set } from './src/idb.js'window.onbeforeunload = () => {  // storage.set(todos)  set('todos', todos)}// обратите внимание, что функция инициализации приложения стала асинхронной;(async () => {  // let todos = storage.get('todos')  let todos = await get('todos')  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Вторую и третью проблемы можно решить только с помощью удаленной базы данных. В качестве таковой мы будем использовать облачную MongoDB. Преимущества ее использования заключаются в отсутствии необходимости предварительной установки и настройки, а также в возможности доступа к данным из любого места. Из недостатков можно отметить отсутствие гарантии конфиденциальности данных. Однако, при желании, данные можно шифровать на клиенте перед отправкой на сервер или на сервере перед отправкой в БД.

React, Vue

Ниже приводятся примеры реализации клиентской части тудушки на React и Vue.

React:


Vue:


База данных


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

  1. Создаем аккаунт в MongoDB Atlas
  2. Во вкладке Projects нажимаем на кнопку New Project
  3. Вводим название проекта, например, todos-db, и нажимаем Next
  4. Нажимаем Create Project
  5. Нажимаем Build a Cluster
  6. Нажимаем Create a cluster (FREE)
  7. Выбираем провайдера и регион, например, Azure и Hong Kong, и нажимаем Create Cluster
  8. Ждем завершения создания кластера и нажимаем connect
  9. В разделе Add a connection IP address выбираем либо Add Your Current IP Address, если у вас статический IP, либо Allow Access from Anywhere, если у вас, как в моем случае, динамический IP (если сомневаетесь, выбирайте второй вариант)
  10. Вводим имя пользователя и пароль, нажимаем Create Database User, затем нажимаем Choose a connection method
  11. Выбираем Connect your application
  12. Копируем строку из раздела Add your connection string into your application code
  13. Нажимаем Close












В корневой директории создаем файл .env и вставляем в него скопированную строку (меняем <username>, <password> и <dbname> на свои данные):

MONGO_URI=mongodb+srv://<username>:<password>@cluster0.hfvcf.mongodb.net/<dbname>?retryWrites=true&w=majority

Сервер


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

npm init -y// илиyarn init -yp

Устанавливаем основные зависимости:

yarn add cors dotenv express express-validator mongoose

  • cors отключает политику общего происхождения (одного источника)
  • dotenv предоставляет доступ к переменным среды в файле .env
  • express облегчает создание сервера на Node.js
  • express-validator служит для проверки (валидации) данных
  • mongoose облегчает работу с MongoDB

Устанавливаем зависимости для разработки:

yarn add -D nodemon open-cli morgan

  • nodemon запускает сервер и автоматически перезагружает его при внесении изменений в файл
  • open-cli открывает вкладку браузера по адресу, на котором запущен сервер
  • morgan логгер HTTP-запросов

Далее добавляем в package.json скрипты для запуска сервера (dev для запуска сервера для разработки и start для продакшн-сервера):

"scripts": {  "start": "node index.js",  "dev": "open-cli http://localhost:1234 && nodemon index.js"},

Отлично. Создаем файл index.js следующего содержания:

// подключаем библиотекиconst express = require('express')const mongoose = require('mongoose')const cors = require('cors')const morgan = require('morgan')require('dotenv/config')// инициализируем приложение и получаем роутерconst app = express()const router = require('./server/router')// подключаем промежуточное ПОapp.use(express.json())app.use(express.urlencoded({ extended: false }))app.use(cors())app.use(morgan('dev'))// указываем, где хранятся статические файлыapp.use(express.static(__dirname))// подлючаемся к БДmongoose.connect(  process.env.MONGO_URI,  {    useNewUrlParser: true,    useUnifiedTopology: true,    useFindAndModify: false,    useCreateIndex: true  },  () => console.log('Connected to database'))// возвращаем index.html в ответ на запрос к корневому узлуapp.get('/', (_, res) => {  res.sendFile(__dirname + '/index.html')})// при запросе к api передаем управление роутеруapp.use('/api', router)// определяем порт и запускаем серверconst PORT = process.env.PORT || 1234app.listen(PORT, () => console.log(`Server is running`))

Тестируем сервер:

yarn dev// илиnpm run dev



Прекрасно, сервер работает. Теперь займемся маршрутизацией. Но перед этим определим схему данных, которые мы будем получать от клиента. Создаем директорию server для хранения серверных файлов. В этой директории создаем файлы Todo.js и router.js.

Структура проекта на данном этапе:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    storage.js  script.js  style.cssserver  Todo.js  router.js.envindex.htmlindex.jspackage.jsonyarn.lock (либо package-lock.json)

Определяем схему в src/Todo.js:

const { Schema, model } = require('mongoose')const todoSchema = new Schema({  id: {    type: String,    required: true,    unique: true  },  text: {    type: String,    required: true  },  done: {    type: Boolean,    required: true  }})// экспорт модели данныхmodule.exports = model('Todo', todoSchema)

Настраиваем маршрутизацию в src/router.js:

// инициализируем роутерconst router = require('express').Router()// модель данныхconst Todo = require('./Todo')// средства валидацииconst { body, validationResult } = require('express-validator')/** * наш интерфейс (http://personeltest.ru/away/localhost:1234/api) * будет принимать и обрабатывать 4 запроса * GET-запрос /get - получение всех задач из БД * POST /add - добавление в БД новой задачи * DELETE /delete/:id - удаление задачи с указанным идентификатором * PUT /update - обновление текста или индикатора выполнения задачи * * для работы с БД используется модель Todo и методы * find() - для получения всех задач * save() - для добавления задачи * deleteOne() - для удаления задачи * updateOne() - для обновления задачи * * ответ на запрос - объект, в свойстве message которого * содержится сообщение либо об успехе операции, либо об ошибке*/// получение всех задачrouter.get('/get', async (_, res) => {  const todos = (await Todo.find()) || []  return res.json(todos)})// добавление задачиrouter.post(  '/add',  // пример валидации  [    body('id').exists(),    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    // ошибки - это результат валидации    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    const todo = new Todo({      id,      text,      done    })    try {      await todo.save()      return res.status(201).json({ message: 'Todo created' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }  })// удаление задачиrouter.delete('/delete/:id', async (req, res) => {  try {    await Todo.deleteOne({      id: req.params.id    })    res.status(201).json({ message: 'Todo deleted' })  } catch (error) {    return res.status(500).json({ message: `Error: ${error}` })  }})// обновление задачиrouter.put(  '/update',  [    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    try {      await Todo.updateOne(        {          id        },        {          text,          done        }      )      return res.status(201).json({ message: 'Todo updated' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }})// экспорт роутераmodule.exports = router

Интеграция


Возвращаемся к клиентской части. Для того, чтобы абстрагировать отправляемые клиентом запросы мы также прибегнем к помощи роутера. Создаем файл client/src/router.js:

/** * наш роутер - это обычная функция, * принимающая адрес конечной точки в качестве параметра (url) * * функция возвращает объект с методами: * get() - для получения всех задач из БД * set() - для добавления в БД новой задачи * update() - для обновления текста или индикатора выполнения задачи * delete() - для удаления задачи с указанным идентификатором * * все методы, кроме get(), принимают на вход задачу * * методы возвращают ответ от сервера в формате json * (объект со свойством message)*/export const Router = (url) => ({  // получение всех задач  get: async () => {    const response = await fetch(`${url}/get`)    return response.json()  },  // добавление задачи  set: async (todo) => {    const response = await fetch(`${url}/add`, {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // обновление задачи  update: async (todo) => {    const response = await fetch(`${url}/update`, {      method: 'PUT',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // удаление задачи  delete: async ({ id }) => {    const response = await fetch(`${url}/delete/${id}`, {      method: 'DELETE'    })    return response.json()  }})

Для того, чтобы сообщать пользователю о результате выполнения CRUD-операции (create, read, update, delete создание, чтение, обновление, удаление), добавим в src/helpers.js еще одну вспомогательную функцию:

// функция создает модальное окно с сообщением о результате операции// и удаляет его через две секундыexport const createModal = ({ message }) => {  root.innerHTML += `<div data-id="modal">${message}</div>`  const timer = setTimeout(() => {    root.querySelector('[data-id="modal"]').remove()    clearTimeout(timer)  }, 2000)}

Вот как выглядит итоговый вариант client/script.js:

import Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, createModal, todosExample } from './src/helpers.js'// импортируем роутер и передаем ему адрес конечной точкиimport { Router } from './src/router.js'const router = Router('http://localhost:1234/api')const App = (root, todos) => {  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  updateCounter()  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал  async function addTodo() {    if (!input.value.trim()) return    const todo = {      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // добавляем в БД новую задачу и сообщаем о результате операции пользователю    createModal(await router.set(todo))    clearInput()    updateCounter()  }  async function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    renderItem(item, todo)    // обновляем индикатор выполнения задачи    createModal(await router.update(todo))    updateCounter()  }  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    $addBtn.textContent = 'Update'    $addBtn.addEventListener(      'click',      async (e) => {        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        // обновляем текст задачи        createModal(await router.update(todo))        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  async function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    // удаляем задачу    createModal(await router.delete(todo))    updateCounter()  }  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дальше все тоже самое  // за исключением window.onbeforeunload  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      case 'active':        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      case 'completed':        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  function updateCounter() {    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  function clearInput() {    input.value = ''    input.focus()  }  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }};(async () => {  // получаем задачи из БД  let todos = await router.get()  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Поздравляю, вы только что создали полноценную фуллстек-тудушку.

TypeScript

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



Заключение


Подведем краткие итоги.

Мы с вами реализовали полноценное клиент-серверное приложение для добавления, редактирования и удаления задач из списка, интегрированное с настоящей базой данных. На клиенте мы использовали самый современный (чистый) JavaScript, на сервере Node.js сквозь призму Express.js, для взаимодействия с БД Mongoose. Мы рассмотрели парочку вариантов хранения данных на стороне клиента (local storage, indexeddb idb-keyval). Также мы увидели примеры реализации клиентской части на React (+TypeScript) и Vue. По-моему, очень неплохо для одной статьи.

Буду рад любой форме обратной связи. Благодарю за внимание и хорошего дня.
Подробнее..

Создание самодокументирующегося сервера на Node.JS

20.01.2021 14:20:49 | Автор: admin

Условия:

  • валидация через Joi

  • использование Typescript

  • Express сервер

  • SWAGGER на /api-docs

Задача: DRY

Решение:

Для начала необходимо решить что первично: схема Joi, Swagger или TypeScript интерфейс. Эмпирическим путём установлено что первичной стоит сделать Joi.

1. Установка всех модулей на Express

npm install --save swagger-ui-express

Добавить строки в app.ts (index.ts):

import swaggerUI = require('swagger-ui-express')import swDocument from './openapi'...app.use('/api-docs',swaggerUI.serve,swaggerUI.setup(swDocument))

2. Создать ./openapi.ts

В этом файле содержится основные сведения о сервере. Создать его (как и все схемы, приведённые ниже) можно с помощью SWAGGER-утилиты. Важно выбрать при этом протокол openapi v3.0.0

Пример содержимого:

import {swLoginRoute} from './routes/login'const swagger = {  openapi: '3.0.0',  info: {    title: 'Express API for Dangle',    version: '1.0.0',    description: 'The REST API for Dangle Panel service'  },  servers: [    {      url: 'http://localhost:3001',      description: 'Development server'    }  ],  paths: {    ...swLoginRoute  },}export default swagger

Пути забираются из роутеров через инклуды.

3. Написать спецификацию роутера

В каждом роутере добавить openapi-описание

Пример ./routes/login/index.ts:

import {swGetUserInfo} from './get-user-info'import {swUpdateInfo} from './update-info'export const swLoginRoute = {  "/login": {    "get": {      ...swGetUserInfo    },    "patch": {      ...swUpdateInfo    }  }}

Выше описан путь /login, поддеживающий два метода: get и patch. Спецификации методов берутся инлудами из файлов get-user-into.ts и update-info.ts. Эти же файлы у меня содержат сами роуты.

4. Написать спецификацию роута и валидацию данных

Спецификация роута будет создаваться автоматически, на основе Joi-схемы.

Для начала сделаем инклуд будущей схемы в нашем роуте.

Примечание: совершенно не важно как вы располагаете ваши файлы, если соответственно модифицируете инклуды.

Строки из файла update-info.ts, в котором расположен мой роут (код код его самого нам не важен):

import schema, {joiSchema} from './update-info.spec/schema'export const swUpdateInfo = {  "summary": "update the user info",  "tags": [    "login"  ],  "parameters": [    {      "name": "key",      "in": "header",      "schema": {        "type": "string"      },      "required": true    }  ],  "requestBody": {    "content": {      "application/json": {        "schema": {          ...schema        }      }    }  },  "responses": {    "200": {      "description": "Done"    },    "default": {      "description": "Error message"    }  }}// ...далее идёт код роута

Этот JSON-объект можно сгенерить той же Swagger-утилитой, чтобы не мучать себя. Обратите внимание на следующую строку:

"schema": {  ...schema}

Это обеспечивает динамическое подключение нашей схемы.

Теперь можно добавить Joi-валидацию в роуте:

await joiSchema.validateAsync(req.body)

4. Пишем Joi-схему

Установка Joi:

npm install --save joi joi-to-swagger

Пример содержимого файла:

const joi = require('joi')const j2s = require('joi-to-swagger')// Joiexport const joiSchema = joi.object().keys({  mode:    joi.string().required(),  email:   joi.string().email()})// end of Joiconst schema = j2s(joiSchema).swaggerexport default schema

Данный файл осуществляет экспорт Joi-объекта и его swagger-схемы.

Чтож, на данном этапе у нас уже есть самодокументирующийся SWAGGER-сервер и валидация данных. Осталось настроить автоматическую генерацию TypeScript-интерфейсов

5. Генерация интерфейсов TypeScript

npm install --save-dev gulp @babel/register @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-typescript

Задачи на себя возьмёт Gulp. Это самая чувствительная часть системы, которую нужно настроить вручную под структуры вашего проекта. Вот как выглядит gulpfile.ts у меня:

const gulp = require('gulp')const through = require('through2')import { compile } from 'json-schema-to-typescript'const fs = require('fs')const endName = "schema.ts"const routes = `./routes/**/*.spec/*${endName}`function path(str: string) : string{   let base = str   if(base.lastIndexOf(endName) != -1)     base = base.substring(0, base.lastIndexOf(endName))   return base}gulp.task('schema', () => {  return gulp.src(routes)    .pipe(through.obj((chunk, enc, cb) => {      const filename = chunk.path      import(filename).then(schema => { // dynamic import        console.log('Converting', filename)        compile(schema.default, `IDTO`)          .then(ts => {            //console.log(path(filename).concat('interface.ts'), ts)            fs.writeFileSync(path(filename).concat('interface.ts'), ts)          })        })      cb(null, chunk)    }))})// watch serviceconst { watch, series } = require('gulp')exports.default = function() {  watch(routes, series('schema'))}

Скрипт обходит все подкаталоги с названием *.spec внутри каталога с роутера. Там он ищет файлы с именами *schema.ts и создаёт рядом файлы c именами *interface.ts

Заключение

Разумеется, эти большие и сложные JSON-объекты с openAPI-спецификацией пугают, но надо понимать, что они не пишутся вручную, а геренятся SWAGGER-утилитой.

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

Подробнее..
Категории: Javascript , Node.js , Api , Express , Swagger , Openapi , Joi

React за 60 секунд валидация формы

02.02.2021 08:07:36 | Автор: admin


Доброго времени суток, друзья!

В этом небольшом туториале я хочу продемонстировать вам пример клиент-серверной валидации формы.

Клиент будет реализован на React, сервер на Express.

Мы не будем изобретать велосипеды, а воспользуемся готовыми решениями: для валидации формы на стороне клиента будет использоваться react-hook-form (+: используются хуки, русский язык), а на стороне сервера express-validator.

Для стилизации будет использоваться styled-components (CSS-in-JS или All-in-JS, учитывая JSX).

Исходный код примера находится здесь.

Поиграть с кодом можно здесь.

Без дальнейших предисловий.

Клиент


Создаем проект с помощью create-react-app:

yarn create react-app form-validation# илиnpm init react-app form-validation# илиnpx create-react-app form-validation

В дальнейшем для установки зависимостей и выполнения команд я буду использовать yarn.

Структура проекта после удаления лишних файлов:

public  index.htmlsrc  App.js  index.js  styles.jsserver.js...

Устанавливаем зависимости:

# для клиентаyarn add styled-components react-hook-form# для сервера (производственные зависимости)yarn add express express-validator cors# для сервера (зависимость для разработки)yarn add -D nodemon# для одновременного запуска серверовyarn add concurrently

Поскольку styled-components не умеет импотировать шрифты, нам придется добавить их в public/index.html:

<head>  ...  <link rel="preconnect" href="http://personeltest.ru/aways/fonts.gstatic.com" />  <link    href="http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Comfortaa&display=swap"    rel="stylesheet"  /></head>

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

  • Имя
    • от 2 до 10 символов
    • кириллица

  • Email
    • особых требований не предъявляется

  • Пароль
    • 8-12 символов
    • латиница: буквы в любом регистре, цифры, нижнее подчеркивание и дефис


Начнем со стилизации (src/styles.js; для подстветки синтаксиса я использую расширение для VSCode vscode-styled-components):

// импорт инструментовimport styled, { createGlobalStyle } from 'styled-components'// глобальные стилиconst GlobalStyle = createGlobalStyle`  body {    margin: 0;    min-height: 100vh;    display: grid;    place-items: center;    background-color: #1c1c1c;    font-family: 'Comfortaa', cursive;    font-size: 14px;    letter-spacing: 1px;    color: #f0f0f0;  }`// заголовокconst StyledTitle = styled.h1`  margin: 1em;  color: orange;`// формаconst StyledForm = styled.form`  margin: 0 auto;  width: 320px;  font-size: 1.2em;  text-align: center;`// подписьconst Label = styled.label`  margin: 0.5em;  display: grid;  grid-template-columns: 1fr 2fr;  align-items: center;  text-align: left;`// проект поля для ввода данныхconst BaseInput = styled.input`  padding: 0.5em 0.75em;  font-family: inherit;  font-size: 0.9em;  letter-spacing: 1px;  outline: none;  border: none;  border-radius: 4px;`// обычное полеconst RegularInput = styled(BaseInput)`  background-color: #f0f0f0;  box-shadow: inset 0 0 2px orange;  &:focus {    background-color: #1c1c1c;    color: #f0f0f0;    box-shadow: inset 0 0 4px yellow;  }`// поле для отправки данных на серверconst SubmitInput = styled(BaseInput)`  margin: 1em 0.5em;  background-image: linear-gradient(yellow, orange);  cursor: pointer;  &:active {    box-shadow: inset 0 1px 3px #1c1c1c;  }`// проект сообщения с текстомconst BaseText = styled.p`  font-size: 1.1em;  text-align: center;  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);`// сообщение об ошибкеconst ErrorText = styled(BaseText)`  font-size: ${(props) => (props.small ? '0.8em' : '1.1em')};  color: red;`// сообщение об успехеconst SuccessText = styled(BaseText)`  color: green;`// экспорт стилизованных компонентовexport {  GlobalStyle,  StyledTitle,  StyledForm,  Label,  RegularInput,  SubmitInput,  ErrorText,  SuccessText}

Импортируем и подключаем глобальные стили в src/index.js:

import React from 'react'import ReactDOM from 'react-dom'// импортируем глобальные стилиimport { GlobalStyle } from './styles'import App from './App'ReactDOM.render(  <React.StrictMode>    {/* подключаем глобальные стили */}    <GlobalStyle />    <App />  </React.StrictMode>,  document.getElementById('root'))

Переходим к основному файлу клиента (src/App.js):

import { useState } from 'react'// импорт хука для валидации формыimport { useForm } from 'react-hook-form'// импорт стилизованных компонентовimport {  StyledTitle,  StyledForm,  Label,  RegularInput,  SubmitInput,  ErrorText,  SuccessText} from './styles'// компонент заголовкаfunction Title() {  return <StyledTitle>Валидация формы</StyledTitle>}// компонент формыfunction Form() {  // инициализируем начальное состояние  const [result, setResult] = useState({    message: '',    success: false  })  // извлекаем средства валидации:  // регистрация проверяемого поля  // ошибки и обработка отправки формы  const { register, errors, handleSubmit } = useForm()  // общие валидаторы  const validators = {    required: 'Не может быть пустым'  }  // функция отправки формы  async function onSubmit(values) {    console.log(values)    const response = await fetch('http://localhost:5000/server', {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(values)    })    const result = await response.json()    // обновляем состояние    setResult({      message: result,      success: response.ok    })  }  // нажатие кнопки сброса полей в исходное состояние приводит к перезагрузке страницы  function onClick() {    window.location.reload()  }  return (    <>      <StyledForm onSubmit={handleSubmit(onSubmit)}>        <Label>          Имя:          <RegularInput            type='text'            name='name'            // поля являются неуправляемыми            // это повышает производительность            ref={register({              ...validators,              minLength: {                value: 2,                message: 'Не менее двух букв'              },              maxLength: {                value: 10,                message: 'Не более десяти букв'              },              pattern: {                value: /[А-ЯЁ]{2,10}/i,                message: 'Только киррилица'              }            })}            defaultValue='Иван'          />        </Label>        {/* ошибки */}        <ErrorText small>{errors.name && errors.name.message}</ErrorText>        <Label>          Email:          <RegularInput            type='email'            name='email'            ref={register({              ...validators,              pattern: {                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,                message: 'Неправильный адрес электронной почты'              }            })}            defaultValue='email@example.com'          />        </Label>        <ErrorText small>{errors.email && errors.email.message}</ErrorText>        <Label>          Пароль:          <RegularInput            type='password'            name='password'            ref={register({              ...validators,              pattern: {                value: /^[A-Z0-9_-]{8,12}$/i,                message:                  'От 8 до 12 символов: латиница, цифры, нижнее подчеркивание и дефис'              }            })}            defaultValue='password'          />        </Label>        <ErrorText small>          {errors.password && errors.password.message}        </ErrorText>        <SubmitInput type='submit' defaultValue='Отправить' />        {/* обратите внимание на атрибут "as", он позволяет превратить "инпут" в кнопку с аналогичными стилями */}        <SubmitInput as='button' onClick={onClick}>          Сбросить        </SubmitInput>      </StyledForm>      {/* результат отправки формы */}      {result.success ? (        <SuccessText>{result.message}</SuccessText>      ) : (        <ErrorText>{result.message}</ErrorText>      )}    </>  )}export default function App() {  return (    <>      <Title />      <Form />    </>  )}

Метод register() хука useForm() поддерживает все атрибуты тега input. Полный список таких атрибутов. В случае с именем, мы могли бы ограничиться регулярным выражением.

Запускаем сервер для клиента с помощью yarn start и тестируем форму:



Замечательно. Валидация на стороне клиента работает, как ожидается. Но ее всегда можно отключить. Поэтому нужна валидация на сервере.

Сервер


Приступаем к реализации сервера (server.js):

const express = require('express')// body читает тело запроса// validationResult - результат валидацииconst { body, validationResult } = require('express-validator')const cors = require('cors')const app = express()const PORT = process.env.PORT || 5000app.use(cors())app.use(express.json())app.use(express.urlencoded({ extended: false }))// валидаторыconst validators = [  body('name').trim().notEmpty().isAlpha('ru-RU').escape(),  body('email').normalizeEmail().isEmail(),  // кастомный валидатор  body('password').custom((value) => {    const regex = /^[A-Z0-9_-]{8,12}$/i    if (!regex.test(value)) throw new Error('Пароль не соответствует шаблону')    return true  })]// валидаторы передаются в качестве middlewareapp.post('/server', validators, (req, res) => {  // извлекаем массив с ошибками из результата валидации  const { errors } = validationResult(req)  console.log(errors)  // если массив с ошибками не является пустым  if (errors.length) {    res.status(400).json('Регистрация провалилась')  } else {    res.status(201).json('Регистрация прошла успешно')  }})app.listen(PORT, () => {  console.log(`Сервер готов. Порт: ${PORT}`)})

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

Добавим в package.json парочку скриптов server для запуска сервера и dev для одновременного запуска серверов:

"scripts": {  "start": "react-scripts start",  "build": "react-scripts build",  "server": "nodemon server",  "dev": "concurrently \"yarn server\" \"yarn start\""}

Выполняем yarn dev и тестируем отправку формы:





Прекрасно. Кажется, у нас все получилось.

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

Благодарю за внимание и хорошего дня.
Подробнее..

Создаем приложение на Node.JS, Express и Typescript с Jest, Swagger, log4js и Routing-controllers

11.01.2021 10:24:00 | Автор: admin
Это пошаговая инструкция создания приложение на Node.JS, с использованием typescript и express. Новое приложение создается не часто, отсюда забываются шаги по его созданию. И я решил написать некую шпаргалку, в помощь самому себе и другим разработчикам. Помимо шагов, я так же снял небольшие видео ролики для наглядности. Существуют уже готовые фреймворки для Node.JS, которые уже содержат в себе все необходимые пакеты и можно работать с ними, но это уже другой путь. Идея была в том, чтобы не зависить целиком от какого-то фреймворка и в случае необходимости менять одни пакеты на другие.

Итак по шагам:
  1. Простое Web приложение youtu.be/7MIIcFDeSg4
    Ставим в определенном порядке пакеты и Node.JS, а так же прописываем настройки.
    1) node.js download, 2) Create directory for your project, 3) npm init, 4) in package.json "main": "dist/index.js",  "scripts": {    "build": "tsc",    "start": "node ."  }5) npm install --save-dev typescript, 6) in tsconfig.json   {  "compilerOptions": {    "esModuleInterop": true,    "outDir": "dist",    "baseUrl": "."  }}8) npm install express, 9) npm install @types/express, 10) create src folder, 11) create src/index.ts with code below:import express from 'express'const app = express();const port = 5000;app.get('/', (request, response) => {  response.send('Hello world!');});app.listen(port, () => console.log(`Running on port ${port}`));13) npm run build, 14) npm run start, 15) localhost:5000
    

  2. Отладка и инициализация в Node.js youtu.be/hfST0e1ITGw
    Настраиваем режим отладки и создаем .env файл для установки входных значений.
    1) in tsconfig.json add: "sourceMap": true2) int package.json add: "prestart": "npm run build",3) In IntelliJ IDEA in Run/Debug Configurations choose: "npm" and add script4) npm i ts-node-dev --save-dev5) int package.json add: "server:watch": "ts-node-dev --respawn --transpile-only src/index.ts"6) add IntelliJ IDEA npm for "server:watch" script7) npm install dotenv8) in index.ts add: dotenv.config();9) create .env file in root dir of your project and add text below in .env file:PORT = 5000const port = process.env.PORT;
    

  3. Добавление log4js и eslint к приложению на Node.JS youtu.be/qcSpd6N7ZJ8
    1) npm install log4js2) in index.ts file:    import log4js from 'log4js';    ...    const logger = log4js.getLogger();    logger.level = process.env.LOG_LEVEL;    ...4) in .env file: LOG_LEVEL=error5) in index.ts file:    ...    logger.info('log4js log info');    logger.debug('log4js log debug');    logger.error('log4js log error');    ...6) npm install eslint --save-dev7) eslint --init8) "prebuild": "npm run lint"9) "lint:fix": "eslint --cache --ext .ts . --fix",10) "lint": "eslint --cache --ext .ts .",    !!! --cache (only changed), .11) IntelliJ IDEA -- file -- setting -- eslint -- automatic12) "rules": {        "semi": ["error", "always"]    }
    

  4. Routing controllers для Node.js youtu.be/_7z5Zubsdps
    Используем routing-controllers для более удобной работы.
    1) npm install routing-controllers2) npm install reflect-metadata3) npm install express body-parser multer4) npm install class-transformer class-validator5) tsconfig.json   "compilerOptions": {      ...      "emitDecoratorMetadata": true,      "experimentalDecorators": true      ...   }6) in index.ts// const app = express();// logger.info('log4js log info');// logger.debug('log4js log debug');// logger.error('log4js log error');// app.get('/', (request, response) => {//   response.send('Hello world2!');// });7) in index.ts   import { createExpressServer } from 'routing-controllers';   import { UserController } from './UserController';   const app = createExpressServer({     controllers: [UserController], // we specify controllers we want to use});8) controller/user-controller.ts   import { Controller, Get, Param } from 'routing-controllers';   import 'reflect-metadata';   @Controller()   export class UserController {     @Get('/users/:id')     getOne (@Param('id') id: number) {       return 'This action returns user #' + id;     }   }9) http://localhost:3001/users/1
    

  5. Node.JS middleware, interceptor, http context youtu.be/iWUMUa7gTTQ
    1) middleware -- middleware.ts2) middleware.tsexport function loggingBefore (request: any, response: any, next?: (err?: any) => any): any {  console.log('do something Before...');  next();}export function loggingAfter (request: any, response: any, next?: (err?: any) => any): any {  console.log('do something After...');  next();}3) user-controller.ts in class@UseBefore(loggingBefore)@UseAfter(loggingAfter)console.log('do something in GET function...');4) user-controller.ts in function @UseBefore(loggingBefore) @UseAfter(loggingAfter)5) user-controller.ts in function @UseInterceptor(function (action: Action, content: any) {    console.log('change response...');    return content;  })6) npm install express-http-context7) index.ts  const app: Express = express();        app.use(bodyParser.json()); app.use(httpContext.middleware); useExpressServer(app, {   controllers: [UserController] }); app.use((req, res, next) => {   httpContext.ns.bindEmitter(req);   httpContext.ns.bindEmitter(res); });8) middleware.ts loggingBefore    import httpContext from 'express-http-context';        console.log('set traceId = 123');    httpContext.set('traceId', 123);9) middleware.ts loggingAfter    console.log(`tracedId = ${httpContext.get('traceId')}`);
    

  6. Node.JS добавляем post запрос, валидация входных данных, глобальный обработчик ошибок youtu.be/onBVkkLEuw4
    1) in user-controller.ts add:  ...  @Post('/users/:id')  @OnUndefined(204)  postOne (@Param('id') id: number, @Body() info: any) {    console.log(JSON.stringify(info));  }  ...2) in postman http://localhost:3001/users/1 { "country":"Russia", "city":"SPb" }3) model -- info.ts4) import { IsDefined } from 'class-validator';export class Info {  @IsDefined()  country: string;  @IsDefined()  city: string;}8) postOne (@Param('id') id: number, @Body() info: Info) {9) middleware -- global-error-handler.ts10) import { ExpressErrorMiddlewareInterface, Middleware } from 'routing-controllers';@Middleware({ type: 'after' })export class GlobalErrorHandler implements ExpressErrorMiddlewareInterface {  error (error: any, request: any, response: any, next: () => any) {    response.send({ ERROR: error });    next();  }}11) useExpressServer(app, {  controllers: [UserController], // we specify controllers we want to use  middlewares: [GlobalErrorHandler],  defaultErrorHandler: false});
    

  7. Swagger документация в Node.JS приложении youtu.be/-uoIasCbsq8
    1) npm install swagger-ui-express2) tsconfig.json -- "resolveJsonModule": true3) src -- swagger -- openapi.json4) index.tsimport swaggerUi from 'swagger-ui-express';import * as swaggerDocument from '../src/swagger/openapi.json';...app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));5) change port to 3000in .env file set PORT=30006) npm install cors7) npm install @types/cors8) in index.ts import cors from 'cors';...app.use(cors() as RequestHandler);...9) Swagger Editor (example for test project)openapiopenapi: 3.0.1info:  title: test API  version: v1servers:  - url: 'http://localhost:3000'tags:  - name: API functions    description: >-      API functions of our application      paths:  /users/{id}:    get:     summary: returns simple answer from get     tags:       - API functions     parameters:       - name: id         in: path         required: true         description: simple parameter         schema:           type : string           example: '1'     description: parameter id just for test     responses:      '200': #status code       description: OK       content:            document:              schema:                type: string                example: some text    post:     summary: returns simple answer from post     tags:       - API functions     requestBody:        required: true        content:          application/json:            schema:               $ref: '#/components/schemas/Info'                      example:              country: Russia              city: Spb     parameters:       - name: id         in: path         required: true         description: simple parameter         schema:           type : string           example: '1'     description: parameter id just for test     responses:      '204': #status code       description: OKcomponents:  schemas:    Info:      type: object      properties:        country:          type: string        city:            type: string
    

  8. Добавляем Unit тесты на Jest в приложение на Node.JS youtu.be/rCIRpTMVEMM
    0) in global-error-handler.tsresponse.status(error.statusCode || error.httpCode).json(error);    next();1) npm install --save-dev jest2) npm i -D ts-jest @types/jest3) npm i -D ts-jest4) package.json -- {...scripts {..."test:unit": "jest --config=jest.config.js",},...}5) create jest.config.js with code below:process.env.NODE_ENV = 'UNITTEST';module.exports = {    clearMocks: true,    collectCoverage: true,    collectCoverageFrom: [        './src/**/*.ts'    ],    coverageDirectory: '<'rootDir>/test/coverage',    testEnvironment: 'node',    testMatch: ['**/*.test.ts'],    preset: 'ts-jest'};6) .eslintignore*.jsnode_modulesdistcoverage}7) .eslintrc.json{..."env": {   "jest": true} ...} 8) test -- controller -- user-controller.test.tsdescribe('UserController', () => {  afterEach(() => {    jest.restoreAllMocks();  });  it('postOne', () => {    const userController = new UserController();    const testBody = {      city: 'SPb'    };    const res = userController.postOne(1, testBody as Info);    expect(res).toBeUndefined();  });}9) in IDEAadd script - test:unitset in environment - NODE_ENV=UNITTEST10) Simple variant of jest.config.js for IDEA:process.env.NODE_ENV = 'UNITTEST';module.exports = {  clearMocks: true,  collectCoverage: false,  testEnvironment: 'node',  testMatch: ['**/*.test.ts'],  preset: 'ts-jest'};11) npm i -D supertest @types/supertest12) in user-controller.test.ts...let server;...beforeAll(async () => {    server = express();    server.use(bodyParser.json());    useExpressServer(server, {      controllers: [UserController], // we specify controllers we want to use      middlewares: [GlobalErrorHandler],      defaultErrorHandler: false    });  });...it('postOne with validations', done => {    request(server)      .post('/users/1')      .send({        country: 'Russia',        city: 'SPb'      } as Info)      .expect(204)      .end((err, res) => {        if (err) throw new Error(JSON.stringify(res.body));        done();      });  });
    

  9. Использование config для Node.JS, а так же другие полезные пакеты. youtu.be/8ZCHUN-JTck
    Пакет config позволяет устанавливать значения констант при инициализации в зависимости от значения NODE_ENV.
    1) npm install config2) npm install @types/config3) config4) default.yaml PORT: 3000    DEV.yaml PORT: 3001    LOCAL.yaml PORT: 3002 5) index.ts   // const port = process.env.PORT;      const port = config.get('PORT');6) IDEA server:watch -- Environment    NODE_ENV=DEV    NODE_ENV=LOCAL-- packages:husky - коммиты в гитsemantic-release - формат коммитов и контроль версийpretty-quick - запускает prettier на измененных файлахprettier - формат кодаeslint-config-prettier - разрешает конфликты между eslint и prettiereslint-plugin-prettier - запускает prettier как правила eslintmock-socket - мок для вебсокетаjest-websocket-mock - тестирование вебсокетаjest-sonar-reporter - конвертр из формата jest в формат sonarjest-mock-extended - мок объектов и интерфейсовws - вебсокетtypescript-string-operations - String.formatlodash - библиотека дополнительных функций для jshttp-status-codes - константы для HTTP статусовmoment - библиотека работы со временем в jsncp - копирование файловjs-yaml - загрузка yaml файловmongodb - функции для работы с Mongomigrate-mongo - миграция для Mongolog-timestamp - запись даты в логaxios - HTTP клиентapplicationinsights - интеграция с Azure Application Insights
    

Подробнее..

NestJS. Загрузка файлов в S3 хранилище (minio)

13.08.2020 02:13:49 | Автор: admin
NestJS фреймворк для создания эффективных, масштабируемых серверных приложений на платформе Node.js. Вы можете встретить утверждение, что NestJS является платформо-независимым фреймворком. Имеется в виду, что он может работать на базе одного из двух фрейморков по Вашему выбору: NestJS+Express или NestJS+Fastify. Это действительно так, или почти так. Эта платформо-независимость заканчивается, на обработке запросов Content-Type: multipart/form-data. То есть практически на второй день разработки. И это не является большой проблемой, если Вы используете платформу NestJS+Express в документации есть пример работы для Content-Type: multipart/form-data. Для NestJS+Fastify такого примера нет, и примеров в сети не так уж и много. И некоторые из этих примеров идут по весьма усложненному пути.

Выбирая между платформой NestJS+Fastify и NestJS+Express я сделал выбор в сторону NestJS+Fastify. Зная склонность разработчиков в любой непонятной ситуации вешать на объект req в Express дополнительные свойства и так общаться между разными частями приложения, я твердо решил что Express в следующем проекте не будет.

Только нужно было решить технический вопрос с Content-Type: multipart/form-data. Также полученные через запросы Content-Type: multipart/form-data файлы я планировал сохранять в хранилище S3. В этом плане реализация запросов Content-Type: multipart/form-data на платформе NestJS+Express меня смущала тем, что не работала с потоками.

Запуск локального хранилища S3



S3 это хранилище данных (можно сказать, хотя не совсем строго, хранилище файлов), доступное по протоколу http. Изначально S3 предоставлялся AWS. В настоящее время API S3 поддерживается и другими облачными сервисами. Но не только. Появились реализации серверов S3, которые Вы можете поднять локально, чтобы использовать их во время разработки, и, возможно, поднять свои серверы S3 для работы на проде.

Для начала нужно определиться с мотивацией использования S3 хранилища данных. В некоторых случаях это позволяет снизить затраты. Например, для хранения резервных копий можно взять самые медленные и дешевые хранилища S3. Быстрые хранилища с высоким трафиком (трафик тарифицируется отдельно) на загрузку данных из хранилища, возможно, будут стоить сравнимо с SSD дисками того же объема.

Более весомым мотивом является 1) расширяемость Вам не нужно думать о том, что место на диске может закончиться, и 2) надежность сервера работают в кластере и Вам не нужно думать о резервном копировании, так как необходимое количество копий есть всегда.

Для поднятия реализации серверов S3 minio локально нужен только установленный на компьютере docker и docker-compose. Соответсвующий файл docker-compose.yml:

version: '3'services:  minio1:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data1-1:/data1      - ./s3/data1-2:/data2    ports:      - '9001:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio2:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data2-1:/data1      - ./s3/data2-2:/data2    ports:      - '9002:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio3:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data3-1:/data1      - ./s3/data3-2:/data2    ports:      - '9003:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio4:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data4-1:/data1      - ./s3/data4-2:/data2    ports:      - '9004:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3


Запускаем и без проблем получаем кластер из 4 серверов S3.

NestJS + Fastify + S3



Работу с сервером NestJS опишу с самых первых шагов, хотя часть этого материала отлично описана в документации. Устанавливается CLI NestJS:

npm install -g @nestjs/cli


Создается новый проект NestJS:

nest new s3-nestjs-tut


Инсталлируются необходимые пакеты (включая те что нужны для работы с S3):

npm install --save @nestjs/platform-fastify fastify-multipart aws-sdk sharpnpm install --save-dev @types/fastify-multipart  @types/aws-sdk @types/sharp


По умолчанию в проекте устанавливается платформа NestJS+Express. Как установить Fastify описано в документации docs.nestjs.com/techniques/performance. Дополнительно нам нужно установить плагин для обработки Content-Type: multipart/form-data fastify-multipart

import { NestFactory } from '@nestjs/core';import {  FastifyAdapter,  NestFastifyApplication,} from '@nestjs/platform-fastify';import fastifyMultipart from 'fastify-multipart';import { AppModule } from './app.module';async function bootstrap() {  const fastifyAdapter = new FastifyAdapter();  fastifyAdapter.register(fastifyMultipart, {    limits: {      fieldNameSize: 1024, // Max field name size in bytes      fieldSize: 128 * 1024 * 1024 * 1024, // Max field value size in bytes      fields: 10, // Max number of non-file fields      fileSize: 128 * 1024 * 1024 * 1024, // For multipart forms, the max file size      files: 2, // Max number of file fields      headerPairs: 2000, // Max number of header key=>value pairs    },  });  const app = await NestFactory.create<NestFastifyApplication>(    AppModule,    fastifyAdapter,  );  await app.listen(3000, '127.0.0.1');}bootstrap();


Теперь опишем сервис, загружающий файлы в хранилище S3, сократив код по обработке некоторых видов ошибок (полный текст есть в репозитарии статьи):

import { Injectable, HttpException, BadRequestException } from '@nestjs/common';import { S3 } from 'aws-sdk';import fastify = require('fastify');import { AppResponseDto } from './dto/app.response.dto';import * as sharp from 'sharp';@Injectable()export class AppService {  async uploadFile(req: fastify.FastifyRequest): Promise<any> {    const promises = [];    return new Promise((resolve, reject) => {      const mp = req.multipart(handler, onEnd);      function onEnd(err) {        if (err) {          reject(new HttpException(err, 500));        } else {          Promise.all(promises).then(            data => {              resolve({ result: 'OK' });            },            err => {              reject(new HttpException(err, 500));            },          );        }      }      function handler(field, file, filename, encoding, mimetype: string) {        if (mimetype && mimetype.match(/^image\/(.*)/)) {          const imageType = mimetype.match(/^image\/(.*)/)[1];          const s3Stream = new S3({            accessKeyId: 'minio',            secretAccessKey: 'minio123',            endpoint: 'http://127.0.0.1:9001',            s3ForcePathStyle: true, // needed with minio?            signatureVersion: 'v4',          });          const promise = s3Stream            .upload(              {                Bucket: 'test',                Key: `200x200_${filename}`,                Body: file.pipe(                  sharp()                    .resize(200, 200)                    [imageType](),                ),              }            )            .promise();          promises.push(promise);        }        const s3Stream = new S3({          accessKeyId: 'minio',          secretAccessKey: 'minio123',          endpoint: 'http://127.0.0.1:9001',          s3ForcePathStyle: true, // needed with minio?          signatureVersion: 'v4',        });        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);      }    });  }}


Из особенностей следует отметить, что мы пишем входной поток в два выходных потока, если загружается картинка. Один из потоков сжимает картинку до размеров 200х200. Во всех случаях используется стиль работы с потоками (stream). Но для того, чтобы отловить возможные ошибки и вернуть их в контроллер, мы вызываем метод promise(), который определен в библиотеке aws-sdk. Полученные промисы накапливаем в массиве promises:

        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);


И, далее, ожидаем их разрешение в методе Promise.all(promises).

Код контроллера, в котором таки пришлось пробросить FastifyRequest в сервис:

import { Controller, Post, Req } from '@nestjs/common';import { AppService } from './app.service';import { FastifyRequest } from 'fastify';@Controller()export class AppController {  constructor(private readonly appService: AppService) {}  @Post('/upload')  async uploadFile(@Req() req: FastifyRequest): Promise<any> {    const result = await this.appService.uploadFile(req);    return result;  }}


Запускается проект:

npm run start:dev


Репозитарий статьи github.com/apapacy/s3-nestjs-tut

apapacy@gmail.com
13 августа 2020 года
Подробнее..
Категории: Node.js , S3 , Nodejs , Docker , Docker-compose , Nestjs , Express , Expressjs , Fastify , Aws-s3 , Minio

Как использовать Websocket на примере простого Express API?

24.08.2020 14:14:04 | Автор: admin

Краткое описание технологии

Websocket это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени.
Для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP. Клиент формирует особый HTTP-запрос, на который сервер отвечает определенным образом.

Примечания.
Несмотря на похожесть новых запросов и ответов на запросы и ответы протокола HTTP, они таковыми не являются. Например, в запросе есть тело, но в заголовках поле Content-Length отсутствует (что нарушает соглашения HTTP). Подробнее об этом можно прочитать в Википедии.
Одним из главных преимуществ технологии это ее простота. На клиенте и сервере есть всего 4 события для обработки:

  1. connection
  2. error
  3. message
  4. close

Почему Websocket?


Кроме ws существуют еще два способа непрерывной передачи данных: Server-Sent Events (SSE) и Long Polling.
Приведем сравнения механизмов непрерывной связи сервера и клиента, а также сделаем выводы, почему стоит (или не стоит) использовать вебсокет.
Websocket sse long pooling
протокол websocket (ws, или wss) HTTP(S) HTTP(S)
скорость высокая низкая низкая
направленность потоков данных двунаправленная однонаправленная двунаправленная
дополнительно передача бинарных данных,
отсутствует поддержка некоторых старых браузеров
автоматическое переподключение при обрыве соединения

Одним из главных преимуществ технологии ws это скорость передачи данных. SSE и LP используют протокол HTTP(S) и работают примерно так:

  1. Делаем запрос на изменения;
  2. Если изменения на сервере появились, то сервер их отправляет;
  3. Когда клиент получает изменения, клиент делает новый запрос.

Выводы:
  1. Вебсокет не требует от клиента постоянно запрашивать изменения и именно поэтому он быстрее.
  2. Вебсокет позволяет передавать бинарные данные, что не позволяет протокол HTTP(S).
  3. Вебсокет не нужно использовать, если проект требует совместимость со старыми версиями браузеров. Читать о совместимости с браузерами

Пример работы простейшего api.


const http = require("http");const express = require( "express");const WebSocket = require( "ws");const app = express();const server = http.createServer(app);const webSocketServer = new WebSocket.Server({ server });webSocketServer.on('connection', ws => {   ws.on('message', m => {webSocketServer.clients.forEach(client => client.send(m));   });   ws.on("error", e => ws.send(e));   ws.send('Hi there, I am a WebSocket server');});server.listen(8999, () => console.log("Server started"))

Что здесь происходит?
Чтобы создать сервер поддерживающий ws, мы создаем обычный http сервер, а потом привязываем к нему при создании websocket сервер.

Функция on помогает управлять событиями websocket. Самым примечательным является событие message, так что рассмотрим его подробнее.

Здесь функция получает параметр m сообщение, то есть то, что отправил пользователь. Таким образом мы можем отправить с клиента строку и обработать ее на сервере. В данном случае сервер просто пересылает это сообщение всем, кто подключен к серверу websocket. Массив clients объекта webSocketServer содержит все подключения к серверу. Объект ws в то же время хранит данные только об одном подключении.

Замечание.

Не стоит использовать такой подход в реальном приложении. Если описать api таким образом, то сервер не может отличить один запрос от другого. О том, как можно построить api на основе websocket будет написано далее.

Взаимодействие с сервером на клиенте будет выглядеть так:
export const wsConnection = new WebSocket("ws://localhost:8999");wsConnection.onopen = function() {    alert("Соединение установлено.");};wsConnection.onclose = function(event) {    if (event.wasClean) {        alert('Соединение закрыто чисто');    } else {        alert('Обрыв соединения'); // например, "убит" процесс сервера    }    alert('Код: ' + event.code + ' причина: ' + event.reason);};wsConnection.onerror = function(error) {    alert("Ошибка " + error.message);};export const wsSend = function(data) {// readyState - true, если есть подключение    if(!wsConnection.readyState){        setTimeout(function (){            wsSend(data);        },100);    } else {        wsConnection.send(data);    }};

API на основе Websocket


В отличие от REST API, где запросы распределены по разным url, Websocket API имеет только один url. Для того, чтобы построить полноценное API на основе вебсокетов, необходимо научить систему отличать один запрос от другого. Это можно реализовать следующим образом:

1) С клиента мы будем передавать запросы в виде строки-json, которую распарсим на сервере:
const sendMessage = (message) => conn.send(JSON.stringify({ event: "chat-message", payload: { userName, message }}));

2) На сервере мы распарсим строку и выделем в ней поле event тип запроса. Пропишем для каждого типа соответствующий ответ:
const dispatchEvent = (message, ws) => {   const json = JSON.parse(message);   switch (json.event) {       case "chat-message": webSocketServer.clients.forEach(client => client.send(message));       default: ws.send((new Error("Wrong query")).message);   }}

Таким образом мы можем отправлять разные запросы на сервер и обрабатывать ответ в зависимости от запроса.

Заключение


Если вам была дана задача сделать API и вы узнали, что поддержка старых браузеров заказчика не интересует, то API на основе WebSocket отличный выбор. Для вашего удобства мы подготовили код клиентской и серверной части по ссылке.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru