Как строить архитектуру приложения node js
Model-view-controller (MVC, «модель-представление-поведение», «модель-представление-контроллер», «модель-вид-контроллер») — схема использования нескольких шаблонов проектирования, с помощью которых модель данных приложения, пользовательский интерфейс и взаимодействие с пользователем разделены на три отдельных компонента таким образом, чтобы модификация одного из компонентов оказывала минимальное воздействие на остальные. Данная схема проектирования часто используется для построения архитектурного каркаса, когда переходят от теории к реализации в конкретной предметной области
Это не единственное отличие, которое может быть в MVC, их ровно столько, сколько может позволить себе любой разработчик, но концепция MVC едина для всех.
Модульность в Node.js
Отсюда мы понимаем, что для того чтобы создать масштабируемое приложение нам будет необходимо разделить его на отдельные модули и подключать их к программе по мере необходимости и Node.js позволяет нам создавать собственные модули. Для примера разделим выше приведенный простой код на модули и получим идентичный результат. Чтобы разбить код на модули нам необходимо создавать файлы модулей, к примеру, мы создадим новый модуль под названием server в файле serevr.js и поместим в нее весь код работы с запросами к серверу, а в файле index.js мы запустим наш пользовательский модуль server. Название модуля соответствует названию файла без расширения .js, код в файле server.js будет следующим
А в файле index.js запишем код подключения модуля server и ее запуска
Запускаем наш пример командой
После этого идем в браузер и набираем адрес нашего запущенного хоста
На этом пример создания простого модуля закончено.
Роутер и обработка запросов
В этой статье мы рассмотрим как строиться архитектура Node.js, думаю многим будет интересно.
Также посмотрите статью «Основы node.js» советую всем прочитать, особенно новичкам.
Aрхитектура Node.js:
Node.js используется как крупными, солидными компаниями, так и недавно созданными стартапами. Платформа с открытым исходным кодом и полностью бесплатная, используется тысячами разработчиков по всему миру.
Веб-приложения:
1. Клиент:
Пользователь взаимодействует с интерфейсной частью веб-приложения. Интерфейс обычно разрабатывается с использованием таких языков, как стили HTML и CSS, наряду с широким использованием фреймворков на основе JavaScript, таких как ReactJS и Angular, которые помогают в проектировании приложений.
2. Сервер:
3. База данных:
Архитектура сервера Node.js:
Node.js использует архитектуру «однопоточного цикла событий» для обработки нескольких одновременных клиентов. Модель обработки Node.js основана на модели событий JavaScript вместе с механизмом обратного вызова JavaScript.
Части архитектуры Node.js:
1. Запросы
2. Сервер Node.js
Это серверная платформа, которая принимает запросы от пользователей, обрабатывает их и возвращает ответы соответствующим пользователям.
3. Очередь событий
Хранит входящие клиентские запросы и передает их один за другим в цикл событий.
4. Пул потоков
Состоит из всех потоков, доступных для выполнения некоторых задач, которые могут потребоваться для выполнения клиентских запросов.
5. Цикл событий
Неограниченно принимает запросы и обрабатывает их, а затем возвращает ответы соответствующим клиентам.
6. Внешние ресурсы
Для блокирования клиентских запросов требуются внешние ресурсы. Эти ресурсы могут быть для вычислений, хранения данных и т. Д.
Рабочий процесс архитектуры Node.js:
1. Клиенты отправляют запросы на веб-сервер для взаимодействия с веб-приложением. Запросы могут быть неблокирующими или блокирующими:
- Запрос данных
- Удаление данных
- Обновление данных
2. Node.js получает входящие запросы и добавляет их в очередь событий.
3. Затем запросы передаются один за другим через цикл событий. Он проверяет, достаточно ли просты запросы.
4. Event Loop обрабатывает простые запросы (неблокирующие операции), такие как опрос ввода-вывода, и возвращает ответы соответствующим клиентам.
Один поток из пула потоков назначается одному сложному запросу. Этот поток отвечает за выполнение конкретного запроса на блокировку путем доступа к внешним ресурсам, таким как вычисления, база данных и т. Д.
После того, как задача выполнена полностью, ответ отправляется в цикл событий, который, в свою очередь, отправляет этот ответ обратно клиенту.
Преимущества архитектуры Node.js:
1. Обработка нескольких одновременных клиентских запросов выполняется быстро и легко.
2. Благодаря использованию очереди событий и пула потоков сервер Node.js позволяет эффективно обрабатывать большое количество входящих запросов.
3. Нет необходимости создавать несколько потоков.
4. Цикл событий обрабатывает все запросы один за другим, поэтому нет необходимости создавать несколько потоков. Вместо этого для обработки блокирующего входящего запроса достаточно одного потока.
Одной из болезней Node.js комьюнити это отсутствие каких либо крупных фреймворков, действительно крупных уровня Symphony/Django/RoR/Spring. Что является причиной все ещё достаточно юного возраста данной технологии. И каждый кузнец кует как умеет ну или как в интернетах посоветовали. Собственно это моя попытка выковать некий свой подход к построению Node.js приложений.
Несколько слов что такое архитектура (IMHO):
Несколько слов про разделение приложения на слои:
Обычно в слое контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Но иногда, а может и немного чаще в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного use case . Назовем этот слой Action layer .
- Минималистичный контроллер - маппинг экшенов на роуты
- Экшен - определяет правила валидации входящих данных, проверки прав доступа(владелец/не владелец/админ/не админ/аноним. ) и бизнес логику
- Data layer - прослойка к БД
В этом мне помогает небольшой Assertion class. Из существующих библиотек есть node-assert-plus.
Assertion это набор статических методов для базовой проверки типов аргуметров и возможно некоторых дополнительных опций (аля < required: true, notEmpty: true, positive: true >).
Все параметры неободимые для работы приложения должны хранится в едином конфиге. Параметры конфига делятся на два типа: констаннты и переменные. Первые храним как обычные поля, вторые извлекаем из переменных окружения (для примера это: логины/пароли доступа в БД, ключи для шифрования сессий или JWT токенов, ключи доступа к другим внешним ресурсам итд. ).
set первым аргументом принимает название переменной окружения, вторым ф-цию валидатор (будь то joi правило или обычная ф-ция возвращающая boolean ) и третий опциональный: аргумент по умолчанию. Таким образом система следит что бы все значения конфига были валидны. При условии невалидности приложение не стартует и выбросит исключение.
Жизнь приложения начинается с класса Server. Класс отвечает за:
- Создание веб сервера(в данном случае express.js )
- Инициализию дефолтных мидлварей( bodyParser , helmet , etc.)
- Инициализию контроллеров(роутеров)
- Инициализию дефолтного обработчика ошибок ( errorMiddleware )
- Отслеживание глобальных ошибок/исключений( uncaughtException , unhandledRejection , etc.)
Компоненты(мидлвари, роутеры, провайдеры) приложения обязаны инициализироватся асинхронно друг после друга. Последовательная инициализация позволяет подготовить зависимые/асинхронные данные для использования другими компонентами.
Прослойка промежуточных обработчиков запросов. Основаня задача дополниение контекста запроса/ответа. Например:
- Получение мета данных пользователя из JWT и добавление их в req.currentUser
- Санитизация запросов
- Установка CORS в ответ( res )
Плохая практика навешивать в мидлвари какую либо бизнес логику или тяжолые блокирующие операции.
Основные задачи контроллера:
- Роутинг
- Взаимовоздействие с пользователем(принимает запрос, оправляет ответ)
- Вызывает Action
- Устанавливает хедеры
Рассмотрим более подробно что из себя представляет метод actionRunner . Если посмотреть на стандартное определение роута в Express.js.
Мы увидим что для работы нам понадобится передать в ф-цию маппинга роута несколько параметров это сам путь( '/api/users' ) и один или несколько обработчиков. У каждого обработчика есть доступ к двум(на самом деле их больше но в данный момент нас интересуют только первые два) аргументам: req - объект запроса и res - объект ответа.
За что и отвечает actionRunner :
- Передает данные из запроса в бизнес логику (обработчик экшена: метод run ) и вызывает ее.
- Забирает результат вычисления и отправляет( req.json(data) ) пользователю
- В случе ошибки: собираются метаданные и передаются дальше для логирования
Выходит такой Lifecycle >> Запрос проходит дефолтные мидлвари >> Попадает в контроллер >> Попадает в actionRunner тот проверяет права доступа текущего юзера к экшену, валидирует запрос и стартует процесс обработки (статическая ф-ция run ) >> В экшене выполняется бизнес логика и результат возращается в контроллер >> actionRunner получает результат и возвращает его клиенту.
Что не стоит делать в контроллере:
- Валидировать параметры( req.params ). Как показала практика это плохая идея. Проверку параметров лучше делать непосредственно в экшене. Таким образом в дальнейшем будет более наглядно видно какие парметры в запросе доступны экшену.
Основным ключевым моментом является использование отдельного класса для каждого эндпоинта. Я определяю понятие Action как класс инкапсулирующий всю логику работы эндпоинта. То есть для реализации круда у нас будет 5 файлов ( CreatePostAction , GetPostByIdAction , UpdatePostAction , DeletePostAction , ListPostsAction ) по экшену на каждый эндпоинт.
Каждый экшен обязан имплементировать такой контракт:
- Статический метод run в задачи которого входит выполнение бизнес логики. Его и вызывает actionRunner .
- Геттер validationRules - объект выполняющий роль схемы валидации входящих параметров экшена.
- Геттер accessTag - тег по которому специальный сервис проверяет права доступа.
Правила валидации ( validationRules ) импортируются из модели или в случае необходимости используются кастомные.
Роль модели в экшене: Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми required полями, другая для обновления, третья для обновления указанного набора полей итд. ) было принято решение не указывать в ф-ции валидации модели required требование. Вместо этого required флаг имеет место быть в момент валидации как опция класса RequestRule .
Повторюсь таким образом мы избегаем черезмерного колличества однотипных моделей используя в экшене непосрественно те правила которые необходимы для конкретного юзкейса.
В итоге имеем один класс(один файл) в котором сосредоточена все логика эндпоинта
Все эшнены являются framework agnostic это значит что в экшенах отсуствует код относящийся к веб-фреймворку. Что позволеят нам переиспользовать экшены как в других проектах так и с другими фреймворками.
p.s. А еще разделение логики на отдельные экшены, облегчает командную работу над проектом. Меньше конфликтов при слиянии веток :)
В данном подходе модель представляет собой исключительно набор полей и правил валидации без какой либо бизнес логики и доп. ф-ционала.
Каждое поле модели это инстанс класса Rule состоящий из валидатора и описания. Выполнение ф-ции валидации обязано вернуть либо булево значение либо строку( error.message )
Все последующие проверки полей в других компонентах приложения обязаны импортироваться из схемы модели. Например в cлое DAO в неком getById(id) вместо того что-бы делать:
Стоит лучше взять правило из схемы модели:
Агенты - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем S3Agent добавляем в него свои методы-обертки. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения.
Перехваченные ошибки внешних API выбрасываем предватительно обернув их в свой кастомный класс ошибки (дабы иметь единый интервейс работы с ошибками).
Как быть в ситуации когда необходимо использовать один и тот же агент в нескольких экшенах ? Для этого случая предназанчен слой Providers . Дабы не плодить для каждого экшена свой агент создаем необходимое единожды в провайдере, а дальше импортируем его в нужных местах.
Хелперы отвечают за всевозможный процессинговые и утилитарные ф-ции(шифрование, JWT) В своем большинстве реализованные через промис.
Работа с ошибками организована через единственнный кастомный класс ошибки и список эррор кодов (хранящийся в виде конфига), это позволяет не создавать на каждый тип ошибки свой класс.
В последствии на фронте выводим ошибку из поля message или то что фронтенд посчтитает нужным в зависимости от поля code .
Опишу частую ошибку встречающуюся во многих проектах на просторах интернетаТолько в случае если нам как-то необходимо обработать/дополнить ошибку делаем так:
Данный подход рассматривает использование статических прав(hardcode), тоесть необходимости менять их динамически (создавать/редактировать роли со своим уникальным списком прав через БД) отсуцтвует.
При формировании JWT, каждый токен получает в payload роль пользователя по которой в дальнейшем просиходит проверка прав через сопоставление роли списку доступных ей эксес-тегов. Любой запрос с невалидным токеном или без него система идентифицирует как ROLE_ANONYMOUS .
Как было сказано выше у каждого экшена есть свой accessTag . Это обычный стринговый ключ состоящий из двух частей название ресурса и название действия(например posts:create ).
Определяем список прав каким ролям доступны какие эксес-теги.
Обязательная дефотлная проверка ( actionTagPolicy ): это проверка права на обращение к экшену, она происходит в контроллере (в методе actionRunner ).
Все дальнейшие проверки происходят непосредственно в экшене: в зависимости от требований, будь то необходимость проверить является ли пользователь владельцем ресурса ли что-то еще дополнительное.
При обращении к айтему(чтение/get by id) необходимо проверить айтем на приватность. В позитивном случае отдаем его только владелецу.
Проверка оборачивается в промис дабы избежать лишних if-else конструкций в экшене при использовании.
При удалении или изменении проверяем является ли текущий юзер владельцем айтема.
Привет, меня зовут Андрей, я Engineering Manager в компании Uptech. В этой статье хочу рассказать об одном из архитектурных подходов для создания приложений — гексагональной архитектуре. Рассмотрим пример ее использования для создания Node.js-приложения.
Если ищете способ улучшить поддерживаемость и тестируемость кода, тогда эта статья для вас.
TLDR. Разделяйте бизнес-логику и любые внешние зависимости — будьте счастливы.
Перед тем как начать, давайте разберемся, какие проблемы нужно решить. Основное — сделать код более поддерживаемым. А именно: быстро добавлять новые фичи, легко менять одни зависимости на другие и тестировать код. Фактически минимизировать технический долг в долгосрочной перспективе. Гексагональная архитектура при правильном использовании может удовлетворить наши желания.
Рассмотрим типичное приложение:
В более общем случае:
Вроде все хорошо, но что обычно происходит в реальной жизни?
В разных доменных частях бизнес-логики появляется все больше и больше зависимостей. Сама бизнес-логика вызывает части инфраструктуры (например, запросы в базу данных или вызов сторонних сервисов) напрямую в разных частях приложения.
В итоге получаем жесткую зависимость между бизнес-логикой и инфраструктурой. В результате тестирование становится достаточно сложным и болезненным для разработчика.
Хотелось бы достичь независимости компонентов приложения и особенно доменной логики от инфраструктуры. Посмотрим, как этого можно добиться.
Порты и адаптеры
Если посмотрим на описанный выше флоу, увидим, что роуты — это просто точки «входа» к бизнес-логике, а база данных и другие сторонние сервисы — «выходы» из приложения. И таких точек «входа» и «выхода» может быть много. Заметив это, Алистер Коберн в 2005 году предложил подход к архитектуре приложений, который назвал «гексагональная архитектура», или «архитектура портов и адаптеров».
В ее основе лежит идея изолирования бизнес-логики от любых внешних зависимостей с помощью концепта так называемых портов и адаптеров. Давайте разберемся, что они из себя представляют.
Наша основная цель — это изоляция бизнес-логики. Но нужен способ взаимодействия с ней. Порт служит именно для этой цели. Порт — описанная спецификация взаимодействия уровня логики с любыми внешними зависимостями. Это просто интерфейс, по которому бизнес-логика вызывается или сама вызывает внешние зависимости, без каких-либо деталей имплементации. Для большинства языков порт — это интерфейс и DTO (Data transfer object), связанные с этими интерфейсами. Порты принадлежат уровню бизнес-логики.
Основная разница между ними в том, как они взаимодействуют с уровнем бизнес-логики. Primary-адаптеры втягивают в себя уровень логики и вызывают его напрямую. Они фактически говорят нашему приложению, что делать. А Secondary-адаптеры имплементируют порт и после этого инжектятся в уровень логики с помощью Dependency injection.
Выглядит это приблизительно так:
Выглядит красиво, но рассмотрим на примере. Спроектируем небольшое приложение календаря.
В данном случае метод CreateEvent обрабатывает POST-запросы по роуту /event, парсит все параметры запроса и вызывает eventCreationService, который есть частью слоя с бизнес-логикой. Он принимает на вход только значения и ничего не знает об особенностях транспорта, что его вызвал, таких как тело запроса или заголовки.
Таким образом мы убираем зависимость бизнес-логики от транспортного протокола и фреймворка:
Порт. Сервис нотификаций
Наш календарь отсылает уведомления и напоминания, и для этого слою бизнес-логики нужно уметь отправлять нотификации с помощью стороннего сервиса. Чтобы не зависеть от конкретного провайдера нотификаций, спрячем его за порт.
Порт принадлежит сервису и используется на уровне бизнес-логики. В сервисе следует заинжектить конкретную имплементацию интерфейса. Это рассмотрим ниже.
Вторичный адаптер. Сервис нотификаций
Теперь осталось описать, как именно оправлять нотификации с помощью конкретного провайдера. Для этого пишем вторичный адаптер. Он имплементирует описанный выше интерфейс NotificationService. В нем с использованием Firebase SDK имплементируем отправку нотификаций через конкретный сервис. Таким образом мы изолируем зависимость на Firebase SDK в одном месте нашего приложения. Адаптер нужно зарегистрировать в чтобы потом заинжектить его в сервисе с бизнес-логикой.
Inversion of Control
Обратите внимание: чтобы сделать вторичные адаптеры независимыми от бизнес-логики, используем Dependency injection (DI). Получается, что зависимости направлены к Application core. Работает принцип Inversion of Control сразу на уровне архитектуры.
Организация Application core
На этом этапе классическая гексагональная архитектура, описанная Алистером Коберном, заканчивается. А у нас большая часть приложения остается не организованной. Собственно, это «гексагон» с бизнес-логикой — Application core. Я поделюсь нашим подходом, как мы это делали. Для нас это неплохо сработало. Но фактически организация кора не является стандартизированной, поэтому эта часть полностью на ваше усмотрение.
Для организации бизнес-логики мы объединили подход слоевой архитектуры (Layered Architecture) и DDD (Domain Driven Design).
Вот что из этого вышло. Следуя слоевому подходу, мы разделили Application core на два слоя: Application и Domain. Domain отвечает за бизнес-логику, в нем хранятся доменные модели и сервисы, которые отражают бизнес-процессы. Это базовый слой, он не зависит от других частей приложения.
Слой Application связывает доменную логику с внешними сервисами. Application-уровень описывает полноценные пользовательские сценарии (use case). Здесь живут порты, через которые происходит взаимодействие с адаптерами. Запросы в базу и на другие сторонние сервисы вызываются из этого уровня.
Сервис Аpplication-уровня работает приблизительно так: он инжектит в себя вторичные адаптеры, не зная ничего о конкретной имплементации этих сервисов. В данном примере EventRepository и NotificationService — это порты, описанные на доменном уровне, для которых из инжектится конкретная имплементация взаимодействия с внешними сервисами.
Второй шаг организации Application core — это разделение на компоненты по доменной области. В каждом из них лежат сервисы и модели, которые тесно связаны между собой, но слабо зависимы от других частей приложения. Такой себе Bounding Сontext из DDD. Примеры таких компонентов: User, Reminder, Event и так далее.
В итоге получаем такую схему:
Для уменьшения связности между компонентами можно использовать event-driven подход и реализовать их взаимодействие на основании ивентов.
В результате мы получаем архитектуру, которая позволяет иметь маленькую связность между компонентами и не зависеть на внешние сервисы. Теперь посмотрим, как это тестировать.
Тесты
У нас было 3 вида тестов, каждый отвечал за свою часть архитектуры.
Unit-тесты
Классические unit-тесты для каждого сервиса. С их помощью мы протестировали бизнес-логику в Аpplication core. Они достаточно легковесны и быстро исполняются.
Integration-тесты
Также хотим понимать, что интеграции с внешними сервисами работают хорошо. Для этого не нужно тестировать какие-то огромные куски функционала, достаточно протестировать адаптеры. Сами по себе тесты медленные и тяжелые, поскольку выполняют запросы по сети и иногда требуют поднятия тестовой инфраструктуры. Но так как в адаптерах нет бизнес-логики и они небольшие, здесь не нужно много тестов.
Acceptance-тесты
Когда мы проверили бизнес-логику и интеграции с внешними сервисами, осталось убедиться, как система работает целиком. В этом помогут Acceptance-тесты. Они тестируют полный пользовательский сценарий, но с замокаными вторичными адаптерами. При этом выполняются быстро, так как не взаимодействуют с сетью. Из минусов — тесты могут быть очень большими для сложных сценариев, поэтому их написание и поддержка потребует много усилий. Чтобы сбалансировать усилия и пользу, мы выработали правило: один Acceptance-тест для одного пользовательского сценария и только для основного успешного кейса.
Звучит это все хорошо, а в чем подвох, спросите вы?
Проблемы
Гексагональная архитектура — не серебряная пуля и к тому же имеет ряд проблем, с которыми придется разбираться.
Транзакции
Транзакция — это абстракция уровня базы данных, которая используется на уровне бизнес-логики. Поэтому получаем прямую зависимость на базу данных на уровне Аpplication. А это то, чего хотелось бы избежать. Не все базы данных поддерживают транзакции, поэтому абстрагировать их не выйдет. Транзакции иногда могут требовать сложной логики роллбеков.
Оптимизация
Порой требуется вытащить данные из базы нетривиальным образом. Для этого пишем сложный запрос, описывая бизнес-правила, какие именно данныенужны. В итоге получаем «утечку» логики в адаптер, усложняем адаптер и тем самым лишаем себя части пользы от архитектуры. Приходится искать баланс между хорошей архитектурой и оптимизациями.
Валидация
Не очевидно, где именно должны быть валидации? С одной стороны, валидация — это часть бизнес-логики и должна жить в Аpplication core. С другой стороны, слои должны быть максимально независимы со своей валидацией на каждом уровне. Или достаточно проверить так где они приходят — на первичных адаптерах? Тут нет правильного ответа, выбирайте то, что больше подойдет вашему приложению.
Выводы
- Отделяйте внешние зависимости от бизнес-логики.
- Тестируйте код с разных сторон и не забывайте о здравом смысле.
- В каждом подходе есть свои проблемы, будьте готовы их решать.
- Помните, что не существует одного метода, который решит все проблемы. Оцените, насколько данный подход эффективен именно для вашей задачи. Например, если у вас мало бизнес-логики, наверное, гексагональная архитектура это не лучший выбор.
Полноценный демопроект можете найти на GitHub.
Полный курс по написанию приложений на Node.js и построению масштабируемой и поддерживаемой архитектуры! Node.js - это среда выполнения JavaScript на backend, спроектированная для построения масштабируемых сетевых приложений. На текущий момент его используют уже не только для создания backend for frontend, но и для написания полноценных backend приложений, микросервисов и парсеров сайтов. Он не заменим для создания GraphQL API или выполнения SSR frontend приложений.
В курсе мы разберём его с нуля, поэтому от вас требуется только начальное знание JavaScript. Этот курс отличается от многих тем, что мы не будем просто писать API на express. Наша задача на практике познакомится со всеми концепциями Node.js и написать несколько приложений - простую CLI утилиту прогноза погоды и архитектурно сложное API, где express нам нужен будет только для перенаправления запросов и промежуточных обработчиков. В остальном API будет спроектирована по принципу многоуровневой архитектуры (layer architecture), где мы явно отделим слои обработчиков, контроллеров, сервисов и репозиториев друг от друга. Мы даже напишем свой небольшой framework, который легко можно масштабировать и где компоненты будут максимально отделимы друг от друга.
Именно это позволит вам в полной мере изучить все особенности Node.js, построение архитектуры и получить понимание принципов на которых базируются крупные framework типа NestJS. Мы детально изучим:
Внутреннее устройство Node.js
Работу таймеров и events
CommonJS и ES Modules
Многопоточность и оптимизацию производительности
Работу движка V8
Работу с Node Package Manager
Работу со стандартными библиотеками и переменными окружений
Перевод приложений на TypeScript
Dependency Injection и Inversion of control
Отладку приложения и поиск утечек памяти или проблем производительности
Авторизацию и JWT
Работу с SQL ORM Prisma
Написание unit и e2e тестов
Лекции содержат как теоретическую часть, так и live-code, где мы вместе будем писать и проектировать наши приложения. В конце каждого модуля вас ожидает тест, который позволит укрепить ваши знания, а небольшие упражнения по ходу курса - попрактиковаться писать код.
Читайте также: