- PVSM.RU - https://www.pvsm.ru -
Доброго дня!
Как и обещал, в продолжение своего пет-проекта [1] по созданию грид-компонента опишу здесь создание backend части на таком фреймворке как NestJS, попутно ознакомив читателя с дополнительными инструментами для backend разработки. Код проекта найдете здесь [2]. Статья в основном для новичков, поэтому не пытайтесь найти здесь что-то сверхъестественное.
Сразу сделаю оговорку, что я не являюсь крутым специалистом по данному фреймворку, скорее – большим его любителем. Но почему все-таки NestJS, а не какой-нибудь FastApi [3](Python), Spring Boot [4] (Java) или еще какой-нибудь модный backend фреймворк, которым также пользуется прогрессивная общественность? Все просто — котики!






P.S. можете еще поискать, возможно их там больше.
Найдете - кидайте в комменты.
Шучу конечно, хотя коты в документации [5] очень забавные.
Первая причина заключается в том, что на основной работе приходится периодически работать с данным фреймворком, копаться в чужом говно коде, дебажить микросервисы и т.п. Ну и во-вторых — фреймворк обрел уже достаточно большую популярность, имеет хорошую документацию (кому-то было не лень перевести [6] ее на русский) и для многих является де-факто стандартом качества разработки RESTfull веб-сервисов. Неплохая обзорная статья по фреймворку находится здесь [7].
Ну а теперь к делу!
Писать будем также под Linux (Ubuntu). Необходимо иметь установленным NodeJS, npm (откуда и как установить описал в первой статье [1]) ну и как обычно — желание творить!
Заключение [18]
Для тех, кто ни разу не работал с фреймворком — опишу как его установить. Так же как и для VueJS, для NestJS существует своя CLI, которая сильно упрощает и ускоряет процесс разработки. Согласно инструкций в официальной документации [19] выполним ее установку:
Откроем терминал и выполним командуnpm i -g @nestjs/cli

на момент написания статьи у меня была версия 9.0.0. Вывод в терминале у Вас наверняка будет отличаться, т.к. у меня уже был установлен nest и данной командой я его просто обновил.
Перейдем в директорию, в которой будем создавать проект и выполним команду по созданию нового проекта nest new grid-component-backend:
Среда nest cli запросит выбрать пакетный менеджер. Я выберу npm (и Вам тоже советую) и жмем Enter:

ждем окончания процесса создания проекта:

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

P.S. можете задонатить и порадовать разработчиков, но я, пожалуй, пропущу этот момент =)
Давайте проверим — все ли хорошо. Перейдем в папку с проектомcd grid-component-backend и стартанем наше приложение — npm run start:

Зелененькие логи говорят о том, что все прошло успешно и по адресу http://127.0.0.1:3000 будет доступно наше приложение:

Можно останавливать запущенный сервер (Ctrl+C) и закрывать терминал. Далее будем работать с проектом в VS Code.
В качестве СУБД для нашего небольшого приложения будем использовать легковесную sqlite [20].
Nest также предоставляет возможность [21] каждый раз не перезапускать/билдить проект при внесении изменений. Если Вы как и я используете Nest CLI, то для настройки этой возможности достаточно выполнить следующие действия:
Установить необходимые пакеты [22] с помощью команды npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
В корне проекта создать файл webpack-hmr.config.js и наполнить его следующим содержимым [23]
В файл src/main.ts добавить вот такие данные [24]
В файле package.json подкорректировать [25] команду для запуска проекта
Сохраняем все изменения, останавливаем проект. В дальнейшем для запуска проекта будем использовать команду npm run start:dev .
В целом...эта штука работает, но почему-то иногда выскакивают вот такие ошибки:

видимо не все работает гладко, поэтому webpack иногда просит перезапустить приложение.
Для автоматического перезапуска приложения можно установить свойство autoRestart в значение true в файле webpack-hmr.config.js:

Если кто сталкивался с такой проблемой и знает с чем это связано и как это вылечить — пишите в комментариях.
Добавим необходимые зависимости для работы с sqlite. Как сказано в документации [26] — Nest из коробки предоставляет пакет @nestjs/typeorm для использования TypeORM в качестве работы с базами данных. Установим этот пакет, а также зависимости для работы с СУБД sqlite c помощью команды npm install --save @nestjs/typeorm typeorm sqlite3 . В файле package.json можно увидеть эти изменения [27].
В файл главного модуля добавим настройки подключения к нашей БД. В случае sqlite их будет не очень много:
В итоге файл корневого модуля будет выглядеть следующим образом:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DocumentsModule } from './documents/documents.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: './test_db.sqlite',
synchronize: true,
autoLoadEntities: true,
}),
DocumentsModule // А это уже импорт нашего модуля для работы с доками,
// о нем говорится в следующем разделе
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
В раздел импортов мы добавили модуль (класс) TypeOrmModule, вызвав его метод forRoot() с необходимыми параметрами. Судя по типу возвращаемого значения (см. скриншот):

этот метод возвращает некий динамический модуль, который поможет нам подключиться к БД.
В сам же метод forRoot() мы передали тип используемой СУБД (sqlite) и путь к файлу test_db.sqlite (этот файл БД будет создан автоматически во время очередной сборки приложения), а также свойства synchronize и autoLoadEntities [28]. Подробнее об этих свойствах можно почитать здесь [29].
Приложения на NestJS имеют модульную архитектуру. Подробней можно почитать в уже приведенной мной статье [7] (она небольшая, советую Вам ознакомиться с ней).
Каждый новый модуль по-хорошему должен храниться в отдельной папке с одноименным названием и должен быть зарегистрирован в корневом модуле приложения (файл app.module.ts). Можно все это сделать вручную, но мы воспользуемся Nest CLI, которая сделает все это за нас. Перейдем в папку с проектом и выполним команду nest g module documents:

Как видно из рисунка — эта команда сгенерировала папку documents в папке src, добавила туда файл documents.module.ts и зарегистрировала этот модуль в файле app.module.ts. Вот эти изменения [30].
Далее, создадим папку src/documents/entities для хранения сущностей модуля Document (хоть сущность у нас будет всего одна), а в этой папке — файл document.entity.ts со следующим содержимым:
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Document {
@PrimaryGeneratedColumn()
id: Number;
@Column({ length: 100, unique: true })
cipher: String;
@Column()
createdOn: Date;
@Column()
inArchive: Boolean;
}
Не буду подробно описывать все декораторы, я думаю их названия говорят сами за себя. В любом случае Вам так или иначе придется курить доки по TypeORM [31] для проектирования своих баз данных =).
Зарегистрируем [32] эту сущность в модуле document:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Document } from './entities/document.entity';
@Module({
imports: [TypeOrmModule.forFeature([Document])],
})
export class DocumentsModule {}
Если hot reload работает нормально, то при сохранении и сборке проекта в корневом каталоге будет создан файл базы данных test_db.sqlite.
Убедимся, что в этой БД создана таблица document с необходимыми атрибутами
Для этого нам нужно установить утилиту sqlite3. Инструкцию по установке можно почитать здесь [33].
Перейдем в терминале в папку с проектом и выполним команду sqlite3 test_db.sqlite

Выполним команду .database и убедимся что мы подключились к нашей БД:

Команда .fullschema выведет нам созданную таблицу (DDL) с ее свойствами:

Так что пока все идет по плану.
Добавим в таблицу запись и выведем содержимое:

Еще одна важная составляющая любого RESTfull сервиса — это контроллеры. Если кратко, в NestJS контроллеры [34] нужны для хранения эндпоинтов. Простыми словами, эндпоинт — это адрес (URI), по которому Вы будете обращаться в адресной строке браузера (либо другого web-клиента, например, Postman, о котором мы еще поговорим), чтобы получить доступ к данным приложения, — например, получить все документы из БД, добавить новый документ, обновить существующий, вывести документы по определенному фильтру, удалить один или несколько документов и т.д. Вместе с адресом иногда (я бы сказал чаще всего!) необходимо передавать дополнительные параметры: заголовки, тело запроса, параметры запроса, тип HTTP-запроса и др. Совокупность всех этих эндпоинтов, их грамотное описание, — что и для чего нужно вызывать с описанием всех параметров HTTP-запроса/ответа представляет собой т.н. API-интерфейс [35] Вашего приложения.
Мне очень нравится определение из википедии:
Если программу (модуль, библиотеку) рассматривать как чёрный ящик [36], то API — это набор «ручек», которые доступны пользователю данного ящика и которые он может вертеть и дёргать.
Чтобы создать контроллер — выполним команду nest g controller documents:

Мы получили на выходе файлы documents.controller.ts (сам файл контроллера) и файл для тестирования documents.controller.spec.ts, который нам не понадобится. CLI также зарегистрировала контроллер в модуле для документов и поместила все в папку с соответствующим модулем. Вот коммит [37] этих изменений.
Однако контроллеры необходимы лишь для хранения/описания эндпоинтов. Непосредственно для реализации всей логики нам нужен еще один важный элемент Nest приложения — провайдер [38] (сервис [39]).
Для создания сервиса выполним, как Вы могли уже догадаться nest g service documents. Результат будет абсолютно аналогичен предыдущему, ссылка [40] на коммит.
Перед тем как приступить к реализации контроллера, — создадим в папке src/documents папку dto и добавим в нее два файла [41]:
Файл create-document.dto.ts с таким содержимым:
export class CreateDocumentDto {
id: Number;
cipher: String;
createdOn: Date;
inArchive: Boolean;
}
и paging-document.dto.ts с вот таким:
import { CreateDocumentDto } from './create-document.dto';
export class PagingDocumentDto extends CreateDocumentDto {
paging: {};
sorting: {};
filtering: {};
}
Подробнее про DTO можно почитать тут [42]. Простыми словами, DTO — это вспомогательные объекты (классы), с помощью которых мы будем взаимодействовать с нашими сущностями, которые мы создали ранее в папке entities, т.е. что-то вроде трансфера между сущностями в БД и телом HTTP - запросов (в оф. документации описание DTO как раз и лежит в разделе Request payloads). Также там говорится что лучше определять DTO-объекты как классы, что мы и сделали в вышеприведенных файлах.
В create-document.dto.ts у нас находится класс для работы с документами — как Вы заметили в нем абсолютно такие же свойства, как и в сущности "документ". Класс PagingDocumentDto в файле paging-document.dto.ts наследует CreateDocumentDto и добавляет еще три свойства для пагинации, сортировки и фильтрации. Зачем они нам нужны — узнаем уже совсем скоро.
Добавим в контроллер вот такую строчку (импортировав при этом DocumentService):
constructor(private readonly documentService: DocumentService) {}
Казалось бы...какой-то конструктор. Но данной строчкой мы внедрили в наш контроллер сервис, в котором будет находиться реализация всех методов для работы с документами и тем самым применили на практике такой подход как Dependency injection [43]. Фреймворк NestJS (и не только он) целиком и полностью построен на этом паттерне [44]. А чтобы мы могли использовать в нашем контроллере функционал DocumentService, — он (сервис), в свою очередь, должен быть снабжен декоратором @Injectable() [45], который говорит о том, что сервис можно внедрять. Это уже является реализацией такого принципа как IoS [46], об этом тоже упоминается [39]в документации по Nest.
Ну все, хватит умных слов....поехали дальше!
Для начала создадим два метода (роута), — GET, на который мы будем "стучаться" чтобы получить все документы из БД:
@Get()
findAll() {
return this.documentService.findAllDocuments();
}
и POST для создания документа:
@Post()
create(@Body() createDocumentDto: CreateDocumentDto) {
return this.documentService
.createDocument(createDocumentDto)
.then((response) => {
return response;
})
.catch((error) => {
throw new HttpException('Произошла какая-то ошибка при создании документа =(: ' + error, HttpStatus.INTERNAL_SERVER_ERROR);
})
}
На данном этапе файл documents.controller.ts должен быть таким:
import { DocumentsService } from './documents.service';
import { CreateDocumentDto } from './dto/create-document.dto';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post
} from '@nestjs/common';
@Controller('documents')
export class DocumentsController {
constructor(private readonly documentService: DocumentsService) { }
@Get()
findAll() {
return this.documentService.findAllDocuments();
}
@Post()
create(@Body() createDocumentDto: CreateDocumentDto) {
return this.documentService
.createDocument(createDocumentDto)
.then((response) => {
return response;
})
.catch((error) => {
throw new HttpException('Произошла какая-то ошибка при создании документа =(: ' + error, HttpStatus.INTERNAL_SERVER_ERROR);
})
}
}
Декоратор@Body [47]говорит о том, что параметр createDocumentDto будет передан через тело POST-запроса. В нашем случае тело запроса будет в формате JSON, ключи которого будут совпадать со свойствами класса CreateDocumentDto
Теперь нам осталось реализовать методы findAllDocuments() и createDocument() для получения и создания документов, соответственно.
Вот как будет выглядеть метод для получения документов:
async findAllDocuments(): Promise<Document[]> {
return await this.dataSource.getRepository(Document).find();
}
А вот так для создания:
async createDocument(createDocumentDto: CreateDocumentDto): Promise<InsertResult> {
return await this.dataSource
.createQueryBuilder()
.insert()
.into(Document)
.values([
{
cipher: createDocumentDto.cipher,
createdOn: createDocumentDto.createdOn,
inArchive: createDocumentDto.inArchive,
},
])
.execute();
}
В обоих случаях мы воспользовались преимуществом async/await [48] синтаксиса. В последних версиях TypeORM для доступа к данным необходимо обращаться к объекту DataSource [49] для взаимодействия с БД.
Ну что, пробуем "дергать" наше API...!
Чтобы выполнить GET-запрос для получения документов достаточно открыть любой браузер и в адресную строку вставить 127.0.0.1:3000/documents. Вот, примерно, что должно получиться:

Оказывается Mozilla умеет красиво выводить JSON ответы. Это означает, что все прошло успешно и мы получили ВСЕ документы из базы данных, который у нас всего один.
Ссылка [50] на коммит вышеперечисленных изменений.
Выполнить GET-запрос из предыдущего раздела в браузере не составило особого труда, но как я уже говорил выше, существуют другие типы HTTP запросов, в которые нужно передавать дополнительные параметры — тело, различные заголовки, вложения и т.д.
Наверное, самой распространенной программкой для выполнения HTTP-запросов является Postman. О том как его установить — почитаете в официальной документации [51]. А о его возможностях есть несколько статей на хабре, например тут [52] и тут [53].
Давайте выполним POST-запрос и добавим новый документ в БД. Вот как это выглядит:

curl --location --request POST '127.0.0.1:3000/documents'
--header 'Content-Type: application/json'
--data-raw '{
"cipher": "7e0535f9-666e-45c4-843d-6c366a4a9d80",
"createdOn": "2019-03-18T15:49:00.000Z",
"inArchive": false
}'
Имея подобный cURL — очень легко создать запрос, просто импортировав его в Postman. В нем уже содержатся все необходимые параметры для выполнения запроса. Делается это следующим образом:
Нажимаем "Import":

Переходим на вкладку "Raw text", вставляем cURL и нажимаем "Continue":

На следующей вкладке нажимаем "Import":

Бьютифицируем наш JSON:

Наш запрос готов. Осталось нажать кнопку "Send".
А чтобы создать cURL из имеющегося запроса — нужно нажать в правом-верхнем углу кнопку со знаком "</>", выбрать в выпадалке cURL и копировать готовый текст:

После отправки запроса — в ответе должен прийти id созданной записи с 201-ым кодом ответа, который говорит о том, что все прошло успешно и запись была создана:

Но что, если нам нужно добавить сотню/тысячу записей. Причем желательно, чтобы данные не дублировались. Можно, конечно, написать небольшой скрипт на каком-либо языке программирования с использованием библиотеки для работы с REST, но, раз уж мы начали изучать Postman, сделаем это с его помощью.
Для начала сохраним наш POST-запрос в какой-нибудь папке:

затем изменим тело запроса следующим образом:

в двойных фигурных скобках мы используем т.н. переменные окружения.
Далее перейдем на вкладку "Pre-request Script" и добавим следующий JS код:

Данная вкладка предназначена для того, чтобы добавить JavaScript код, который будет выполнен ПЕРЕД выполнением самого запроса. Тем самым мы имеем возможность задавать переменные окружения в фигурных скобках, определенные нами выше в теле запроса.
Сначала мы определили функцию randomDate(start, end), которая возвращает рандомную дату в заданном интервале (самому было думать лень, поэтому взял код отсюда [54]).
Чтобы назначить переменной значение, необходимо воспользоваться следующим синтаксисом, который нам предоставляет Postman:
pm.environment.set("variable_key", "variable_value");
Задаем переменную createdOn, передав в функцию randomDate начало и конец интервала:
pm.environment.set("createdOn", randomDate(new Date(2012, 0, 1), new Date()));
Генерировать рандомное булевое значение inArchive (а почему бы и нет!) можно вот так:
pm.environment.set("inArchive", Math.random() < 0.5);
Получить уникальный GUID для поля cipher можно следующим образом:
pm.environment.set("cipher", "{{$guid}}");
обязательно сохраняем все изменения:

кликаем правой кнопкой мыши по папке с запросом и выбираем пункт "Run collection":

В открывшемся окне у нас имеется единственный POST-запрос, который мы добавили в папку. Т.к. у нас в БД уже есть 2 записи, зададим 98 итераций и поставим галочку напротив "Save responces" чтобы сохранять ответы (мало ли, вдруг что-то пойдет не так):

Нажимаем "Run grid-component-backend"
Если все пройдет успешно, — Postman выполнит 98 запросов по созданию записей:

Проверить наличие записей в БД можно тремя способами: напрямую выполнить SQL-запрос в БД, выполнить GET-запрос в браузере как в предыдущем разделе, либо создать в постмэне GET-запрос, который должен вернуть нам JSON-массив с добавленными документами.
Покажу как можно получить готовый cURL из вкладки Network браузера для последующего импорта в Postman.
Запустим наш браузер (у меня Mozilla, но то же самое прокатит и для Google Chrome) нажмем клавишу F12 чтобы открыть консоль. Введем как и ранее в адресную строку http://127.0.0.1:3000/documents для выполнения GET-запроса и нажмем Enter. На вкладке Network должен появиться наш запрос:

Если кликнуть по нему правой кнопкой — в контекстном меню можно найти замечательную опцию Copy as cURL, которая сгенерирует вам cURL и сразу добавит в буфер обмена для последующего импорта в Postman:

Данная опция очень полезна для отладки API в готовых проектах.
Так как записей теперь у нас в БД достаточно (целых 100, а могло быть еще больше!) нужно выводить их небольшими порциями. Вообще, пагинация — это отдельная, очень обширная тема, на которую написано немало статей. Погуглив немного по этой теме Вы увидите, что для реализации пагинации применяются в основном два стиля (синтаксиса) — skip-take и limit-offset. Но! это всего лишь два названия одной и той же сущности. Помимо этого каждая СУБД предоставляет свои штатные инструменты для постраничного просмотра данных. Вот ссылка [55] на неплохую статью про пагинацию в Postgres, а вот официальная дока [56] по этой теме.
Первым делом нужно понять, как лучше всего осуществить пагинацию, используя имеющуюся ORM, в нашем случае TypeORM. Давайте же спросим это у гугла, который в первой же ссылке [57] отведет нас на стэковерфлоу. Ну все...осталось только сделать.
Добавим в контроллер следующий метод (по умолчанию POST запрос в NestJS возвращает 201 код ответа, который говорит о создании некоторой записи/объекта, но в данном случае нам не нужно ничего создавать, — нужно просто вернуть записи, поэтому добавим декоратор @HttpCode [58]с соответствующим кодом ответа):
@Post('/findPaginated')
@HttpCode(HttpStatus.OK)
findPaginated(@Body() pagingDocumentDto: PagingDocumentDto) {
return this.documentService.findPaginated(pagingDocumentDto);
}
а в сервис, соответственно, реализацию метода:
async findPaginated(pagingDocumentDto: PagingDocumentDto) {
let options = {
skip: pagingDocumentDto.paging['skip'],
take: pagingDocumentDto.paging['take'],
order: pagingDocumentDto.sorting,
};
const filter = pagingDocumentDto.filtering;
let filters = {};
// Цикл для динамического формирования фильтров
for (var column in filter) {
if (Object.prototype.hasOwnProperty.call(filter, column)) {
const valueToFilter = filter[column]['valueToFilter'];
const comparisonOperator = filter[column]['comparisonOperator'];
const columnType = filter[column]['columnType'];
if (valueToFilter || valueToFilter === false) {
filters[column] = this.operatorMapping(
columnType,
comparisonOperator,
valueToFilter,
);
}
}
}
options['where'] = filters;
const [documents, count] = await this.dataSource.getRepository(Document).findAndCount(
options,
);
return {
documents,
count,
};
}
operatorMapping(
columnType: string,
comparisonOperator: string,
valueToFilter: any,
): FindOperator<any> {
switch (comparisonOperator) {
case 'likeOperator':
if (['number', 'datetime-local'].includes(columnType))
return Raw(
(alias) =>
`lower(cast(${alias} as text)) like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
// if (['string'].includes(columnType))
return Raw(
(alias) =>
`lower(${alias}) like '%${(valueToFilter + '').toLowerCase()}%'`,
);
case 'notLikeOperator':
if (['number', 'datetime-local'].includes(columnType))
return Raw(
(alias) =>
`lower(cast(${alias} as text)) not like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
// if (['string'].includes(columnType))
return Raw(
(alias) =>
`lower(${alias}) not like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
case 'greaterThanOperator':
return MoreThan(valueToFilter);
case 'greaterThanOrEqualOperator':
return MoreThanOrEqual(valueToFilter);
case 'lessThanOperator':
return LessThan(valueToFilter);
case 'lessThanOrEqualOperator':
return LessThanOrEqual(valueToFilter);
case 'notEqualOperator':
return Not(valueToFilter);
default:
// equalOperator
return Equal(valueToFilter);
}
}
Немного комментариев.
С фронта к нам будет приходить тело запроса, содержащее необходимые параметры. Вот пример такого тела:
{
"paging": {
"skip": 0,
"take": "5"
},
"sorting": {
"createdOn": "DESC"
},
"filtering": {
"createdOn": {
"columnType": "datetime-local",
"comparisonOperator": "greaterThanOrEqualOperator",
"valueToFilter": "2016-02-11T14:44"
},
"cipher": {
"columnType": "text",
"comparisonOperator": "likeOperator",
"valueToFilter": "14"
}
}
}
В объект options сохраняем данные для пагинации (skip, take) и сортировки (order):
let options = {
skip: pagingDocumentDto.paging['skip'],
take: pagingDocumentDto.paging['take'],
order: pagingDocumentDto.sorting,
};
в filter сэйвим данные для фильтрации:
const filter = pagingDocumentDto.filtering;
В объект filters будем динамически добавлять параметры для поиска данных в зависимости от фильтруемых столбцов и операторов сравнения:
let filters = {};
// Цикл для динамического формирования фильтров
for (var column in filter) {
if (Object.prototype.hasOwnProperty.call(filter, column)) {
const valueToFilter = filter[column]['valueToFilter'];
const comparisonOperator = filter[column]['comparisonOperator'];
const columnType = filter[column]['columnType'];
if (valueToFilter || valueToFilter === false) {
filters[column] = this.operatorMapping(
columnType,
comparisonOperator,
valueToFilter,
);
}
}
}
В данном цикле мы перебираем все свойства объекта filtering из тела запроса выше и передаем их в метод operatorMapping(), который формирует нам выражение для поиска [59].
Для like-поиска был использован метод Raw. Причем для поиска по столбцам с не текстовыми значениями пришлось кастануть их в текстовый тип, ну и привести к одному регистру:
return Raw(
(alias) =>
`lower(cast(${alias} as text)) like '%${(
valueToFilter + ''
).toLowerCase()}%'`,
);
В конце передаем все параметры в метод findAndCount, который возвращает нам найденные записи и их количество:
const [documents, count] = await this.dataSource.getRepository(Document).findAndCount(
options,
);
Итак, все готово — вода, кипящее масло бэк и фронт. Давайте запустим все это вместе.
Как Вы уже успели заметить — линукс установлен у меня на виртуалке. Это, честно говоря, боль! Поэтому чтобы сильно не нагружать систему — я закрою VS Code и запущу фронт и бэк в терминале.
Переходим в папку с бэкендом и запускаем его:

а теперь фронт (в отдельном терминале):

Если фронт вы делали по инструкции из первой части [1], то после запуска должен запуститься браузер, который отобразит грид и попытается загрузить данные. Если успеть нажать F12 и открыть консоль, то можно увидеть вот такое сообщение:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:3000/documents/findPaginated [60]. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 404.
Попробую объяснить простым языком. Фронт крутится у нас по адресу 127.0.0.1:8080, а бэк на 127.0.0.1:3000. Т.е. используются разные источники (Origins [61]), поэтому браузер пытается добраться до бэкенда через (cross) фронтенд:

но браузеры придерживаются политики одного источника — Same-origin policy [62] (а у нас они разные, т.к. разные порты), поэтому, если не предпринять дополнительные меры, то браузер не позволит выполнить данный запрос на бэкенд.
В NestJS есть отдельная страница [63] по CORS, на которой описано как можно доработать приложение, чтобы браузер не ругался. Есть несколько способов, — я воспользуюсь вот таким (в файл main.ts добавим [64] следующую опцию):

Сделайте такую же доработку, запустите проект с помощью команды npm run start:dev и обновите страницу в браузере:

Отлично! Данные загружены, все работает!
P.S> Мозилла почему-то отображает элементы для пагинации слева (а по задумке должны быть справа) и галочки выровнены по центру, хотя предусматривалось, что они будут слева. Хром же отображает все как и я хотел (как в песочнице [65]). Если есть мысли по данному поведению — оставляйте в комментариях.
Ну вот, наконец-то...я это сделал! Однако, чтобы Вы понимали, — все что описано в этих двух статьях — это даже не надводная часть, а лишь снежная шапка, покрывающая вершину айсберга под названием Web-разработка. Наверное, где-то вот здесь:

Тем не менее, очень надеюсь, что материал этих двух статей был хоть сколько-нибудь полезным и послужит отправной точкой для изучения Web-технологий.
Автор: Зеленый Андрей Сергеевич
Источник [66]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/node-js/378575
Ссылки в тексте:
[1] пет-проекта: https://habr.com/ru/post/676622/
[2] здесь: https://github.com/greenDev7/grid-component-backend
[3] FastApi : https://fastapi.tiangolo.com/
[4] Spring Boot: https://spring.io/projects/spring-boot
[5] документации: https://nestjs.com/
[6] перевести: https://habr.com/ru/company/timeweb/blog/663234/
[7] здесь: https://habr.com/ru/company/domclick/blog/567816/
[8] Установка NestJS и создание проекта: https://habr.com/ru/post/678682/#install-nestcli
[9] Настройка Hot Reload: https://habr.com/ru/post/678682/#hot-reload-setup
[10] Настраиваем проект для работы с sqlite: https://habr.com/ru/post/678682/#sqlite-setup
[11] Создание модуля для работы с документами: https://habr.com/ru/post/678682/#document-module-creation
[12] Создание контроллера и сервиса: https://habr.com/ru/post/678682/#controller-and-service-creation
[13] Data Transfer Object (DTO): https://habr.com/ru/post/678682/#dto
[14] Реализация API. Dependency injection: https://habr.com/ru/post/678682/#api-implementation
[15] Postman. Тестируем API и заполняем БД: https://habr.com/ru/post/678682/#postman
[16] Пагинация, сортировка, фильтрация: https://habr.com/ru/post/678682/#paging-sorting-filtering
[17] Запускаем фронт и бэк. Что-то пошло не так. CORS: https://habr.com/ru/post/678682/#cors
[18] Заключение: https://habr.com/ru/post/678682/#conclusion
[19] официальной документации: https://docs.nestjs.com/#installation
[20] sqlite: https://www.sqlite.org/index.html
[21] предоставляет возможность: https://docs.nestjs.com/recipes/hot-reload
[22] пакеты: https://github.com/greenDev7/grid-component-backend/commit/b8795d66fc788bbbbed05856795c5d4d0b6f52d9#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519
[23] содержимым: https://github.com/greenDev7/grid-component-backend/commit/0280ca996f03fe5ec9ae9abe93d3c744a5052609#diff-daae7b65b9b41cd81aae4709dc7ea01dc706a918806efb5e9c64f460a5ba7afa
[24] данные: https://github.com/greenDev7/grid-component-backend/commit/0280ca996f03fe5ec9ae9abe93d3c744a5052609#diff-4fab5baaca5c14d2de62d8d2fceef376ddddcc8e9509d86cfa5643f51b89ce3d
[25] подкорректировать: https://github.com/greenDev7/grid-component-backend/commit/0280ca996f03fe5ec9ae9abe93d3c744a5052609#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519
[26] документации: https://docs.nestjs.com/techniques/database
[27] изменения: https://github.com/greenDev7/grid-component-backend/commit/8e44992422aeea287016ae7949b7d67c71c4c4cb#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519
[28] autoLoadEntities: https://docs.nestjs.com/techniques/database#auto-load-entities
[29] здесь: https://typeorm.io/data-source-options
[30] изменения: https://github.com/greenDev7/grid-component-backend/commit/2a6339319253ba03cec8f350d88f882ca6f95ad2
[31] доки по TypeORM: https://typeorm.io/entities
[32] Зарегистрируем: https://github.com/greenDev7/grid-component-backend/commit/a04322c10264e32fa048a78c33da61e37817befb
[33] здесь: https://linuxhint.com/install-sqlite-ubuntu-linux-mint/
[34] контроллеры: https://docs.nestjs.com/controllers
[35] API-интерфейс: https://ru.wikipedia.org/wiki/API
[36] чёрный ящик: https://ru.wikipedia.org/wiki/%D0%A7%D1%91%D1%80%D0%BD%D1%8B%D0%B9_%D1%8F%D1%89%D0%B8%D0%BA
[37] коммит: https://github.com/greenDev7/grid-component-backend/commit/5b8b4cb6ff4a0805f6c0be7690918cd8c878f503#diff-5c3abcca324680ec4cb39b0ae614cb8b57a02d1f9d9e47afe71664de6613600e
[38] провайдер: https://docs.nestjs.com/providers
[39] сервис: https://docs.nestjs.com/providers#services
[40] ссылка: https://github.com/greenDev7/grid-component-backend/commit/c31bdbdde28c239dfd53cf5d3772954c4c8ac2e4
[41] два файла: https://github.com/greenDev7/grid-component-backend/commit/835b101d2f930e24ac92d60a0622ea35a8a0fdc8
[42] тут: https://docs.nestjs.com/controllers#request-payloads
[43] Dependency injection: https://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8
[44] паттерне: https://docs.nestjs.com/providers#dependency-injection
[45] @Injectable(): https://www.pvsm.ru/users/injectable()
[46] IoS: https://en.wikipedia.org/wiki/Inversion_of_control
[47] @Body: https://www.pvsm.ru/users/body
[48] async/await: https://developer.mozilla.org/ru/docs/Learn/JavaScript/Asynchronous/Introducing
[49] DataSource: https://github.com/typeorm/typeorm/blob/master/CHANGELOG.md#features-6
[50] Ссылка: https://github.com/greenDev7/grid-component-backend/commit/e8fa8f5ad6747b1a09b0da649609a6f75cb2f036
[51] документации: https://www.postman.com/
[52] тут: https://habr.com/ru/company/maxilect/blog/596789/
[53] тут: https://habr.com/ru/company/kolesa/blog/351250/
[54] отсюда: https://stackoverflow.com/questions/9035627/elegant-method-to-generate-array-of-random-dates-within-two-dates
[55] ссылка: https://habr.com/ru/post/301044/
[56] дока: https://www.postgresql.org/docs/current/queries-limit.html
[57] ссылке: https://stackoverflow.com/questions/53922503/how-to-implement-pagination-in-nestjs-with-typeorm
[58] @HttpCode: https://www.pvsm.ru/users/HttpCode
[59] выражение для поиска: https://github.com/typeorm/typeorm/blob/master/docs/find-options.md
[60] http://127.0.0.1:3000/documents/findPaginated: http://127.0.0.1:3000/documents/findPaginated
[61] Origins: https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Origin
[62] Same-origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
[63] страница: https://docs.nestjs.com/security/cors
[64] добавим: https://github.com/greenDev7/grid-component-backend/commit/5015893a24c8653ca2515731fc48bf9b604ae576
[65] песочнице: https://codesandbox.io/s/grid-component-frontend-ib6rqm
[66] Источник: https://habr.com/ru/post/678682/?utm_source=habrahabr&utm_medium=rss&utm_campaign=678682
Нажмите здесь для печати.