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


Построение двухфакторной аутентификации с помощью NestJS и Postgres
Arctype Team for Arctype ・ Jan 28 ・ 13 min read
В этой статье мы узнаем больше об архитектуре контроллера представления модели (MVC), создав приложение NestJS MVC с YugabyteDB. Мы создадим демонстрационный проект магазина электронных книг. Код для этого руководства доступен в моем репозитории Github. Не стесняйтесь клонировать его по мере выполнения шагов. Давайте начнем!
Проектирование контроллера представления модели
MVC — это архитектурная парадигма, которая делит приложение на три основных логических компонента — модель, представление и контроллер. Каждый из этих компонентов предназначен для обработки определенных частей разработки приложения. MVC — это популярный отраслевой стандарт веб-разработки для создания масштабируемых и гибких проектов.
Три логических компонента MVC выглядят следующим образом:
- Модель: По сравнению с View и Controller, этот уровень считается самым низким. Он представляет собой данные, которые передаются между компонентами View и Controller, и определяет хранение всех элементов данных в приложении.
- Представление: Этот компонент отвечает за пользовательский интерфейс приложения. Он также управляет данными конечного пользователя, а также связью между пользователем и контроллером.
- Контроллер: Контроллер завершает цикл, получая пользовательский ввод, преобразуя его в соответствующие сообщения, передавая их представлениям и управляя обработчиками запросов.
Модель MVC имеет следующие преимущества:
- Он позволяет легко организовывать большие веб-приложения
- Он позволяет легко изменять любую часть приложения, не затрагивая при этом другие части приложения
- Упрощает процесс тестирования кода
- Позволяет легко сотрудничать между командами разработчиков
- Это помогает разработчикам создавать легко поддерживаемый код
- Позволяет разработчикам создавать и использовать предпочитаемые ими движки представления.
Предварительные условия
Этот учебник представляет собой практическую демонстрацию. Чтобы следовать этому руководству, убедитесь, что у вас установлено следующее:
- Arctype
- NodeJS
- База данных Yugabyte
- Postman
Код для этого руководства доступен в моем репозитории Github. Не стесняйтесь клонировать его по мере выполнения шагов.
Что такое NestJS?
NestJS — это фреймворк Node.js для создания быстрых, тестируемых, масштабируемых, слабосвязанных серверных приложений, использующих TypeScript. Он использует преимущества мощных серверных HTTP-фреймворков, таких как Express или Fastify. Nest добавляет уровень абстракции к фреймворкам Node.js и раскрывает их API для разработчиков. Он поддерживает такие системы управления базами данных, как PostgreSQL, MySQL, а в этом учебнике — yugabyteDB. NestJS также предлагает инъекции зависимостей из коробки.
Почему стоит использовать NestJS?
NestJS является одним из самых популярных фреймворков Node.JS с момента его выхода в 2017 году. Некоторые из причин, по которым разработчики используют NestJS, следующие:
- Он обладает высокой масштабируемостью и прост в обслуживании
- У него большое сообщество разработчиков и система поддержки
- Nest нашел уникальное пересечение front-end и middleware программирования, которое многие языки пытаются обнаружить.
- Поддержка TypeScript в NestJS гарантирует, что он останется актуальным в постоянно развивающемся мире JavaScript и обеспечит разработчикам меньше контекстных сдвигов.
- Имеет исчерпывающую документацию
- Простое модульное тестирование
- Он создан для крупномасштабных корпоративных приложений
- Nest предоставляет готовую архитектуру приложений, которая позволяет разработчикам и командам создавать высокотестируемые, масштабируемые, слабосвязанные и легко поддерживаемые приложения.
Настройка проекта
Прежде чем мы погрузимся в кодинг, давайте создадим наш проект NestJS и определим структуру проекта. Начнем с создания папки проекта. Откройте терминал и выполните следующую команду:
mkdir nestmvcapp && cd nestmvcapp
Затем установите NestJS CLI с помощью следующей команды:
npm i -g @nestjs/cli
После завершения установки выполните приведенную ниже команду для создания проекта NestJS.
nest new bookapi
Выберите предпочтительный менеджер пакетов npm. Для этого руководства мы будем использовать npm и подождем, пока необходимые пакеты будут установлены. После завершения установки перейдем к созданию нашей таблицы базы данных с помощью Arctype.
Настройка базы данных YugabyteDB
Чтобы начать использовать базу данных Yugabyte в нашем приложении, нам необходимо установить ее на нашу машину. Давайте посмотрим, как это сделать, шаг за шагом. Сначала убедитесь, что у вас есть Python.
# Ubuntu 20.04
sudo apt install python-is-python3
Затем убедитесь, что у вас установлен wget. Это можно сделать с помощью команды ниже.
sudo apt install wget
Далее загрузите и извлеките базу данных Yugabyte Database:
# download
wget https://downloads.yugabyte.com/releases/2.11.2.0/yugabyte-2.11.2.0-b89-linux-x86_64.tar.gz
# extract
tar xvfz yugabyte-2.11.2.0-b89-linux-x86_64.tar.gz && cd yugabyte-2.11.2.0/
Затем настройте YugabyteDB с помощью приведенной ниже команды.
./bin/post_install.sh
Наконец, запустите вашу базу данных Yugabyte.
./bin/yugabyted start
Теперь подключим Arctype к Yugabyte. Откройте Arctype, перейдите на вкладку YugabyteDB и подключитесь к базе данных Yugabyte, заполнив информацию, как показано на скриншоте ниже:
Обратите внимание, что на скриншоте выше мы оставили вход базы данных пустым. Это потому, что у нас еще нет ни одной созданной базы данных. Итак, давайте создадим одну. Нажмите на кнопку New Query (Новый запрос) и выполните приведенную ниже команду SQL:
CREATE DATABASE books_db;
Установка зависимостей
Когда наша база данных Yugabyte настроена, давайте установим зависимости для нашего приложения. Установите typeorm, pg и ejs с помощью команды ниже:
npm install --save @nestjs/typeorm typeorm pg ejs
Установка займет немного времени, поэтому подождите, пока она завершится. Затем мы можем приступить к созданию нашего приложения.
Создайте модуль Books
Модуль — это класс, аннотированный декоратором @Module(). Nest использует метаданные, предоставляемые декоратором @Module(), для организации структуры приложения. Мы создадим модуль books с помощью команды ниже:
nest generate module books
Приведенная выше команда создаст папку books в папке src с файлом books.module.ts и зарегистрирует его в файле корневого модуля приложения (app.module.ts).
Создание класса модели Books
Создав модуль books, давайте создадим модель для создания и чтения данных из нашей базы данных.
Создайте класс модели книги с помощью команды ниже:
nest generate class /books/model/book --flat
Приведенная выше команда создаст файл model/book.ts в каталоге модуля books. Флаг —flat гарантирует, что Nest не будет генерировать папку для модели books.
Далее давайте определим нашу модель базы данных с помощью Typeorm. Нам нужны поля id, title, author, quantity, description и createdAt для нашей модели книг. Мы будем использовать декоратор Typeorm Entity для определения класса нашей модели, декоратор Column для определения полей, PrimaryGeneratedColumn для создания случайных идентификаторов для наших книг с помощью uuid, и декоратор CreatedDateColumn для сохранения текущей даты-времени, когда книга была создана. Откройте файл model/book.ts и добавьте приведенный ниже фрагмент кода:
import { Entity, Column, PrimaryGeneratedColumn, PrimaryColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Book {
@PrimaryGeneratedColumn("uuid")
id: number;
@Column()
title: string;
@Column()
author: string;
@Column()
quantity: number
@Column()
description: String
@CreateDateColumn()
createdAt: Date;
}
Когда мы запустим наше приложение, Typeorm сгенерирует SQL-эквивалент модели, чтобы создать таблицу книг в нашей базе данных Yugabyte.
Далее мы подключим наше приложение к базе данных Yugabyte в файле src/app.module.ts. Сначала импортируйте модуль Nest TypeOrmModule и класс модели Books с помощью приведенного ниже фрагмента кода:
import { TypeOrmModule } from '@nestjs/typeorm'
import { Book } from './movie/model/book';
Затем подключитесь к базе данных с помощью метода forRoot с нашими учетными данными базы данных с помощью приведенного ниже фрагмента кода:
imports: [
…
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
username: 'yugabyte',
port: 5433,
password: '',
database: 'books_db',
entities: [Book],
synchronize: true,
}),
],
Нам также необходимо экспортировать наш класс модели Books в файле books.module.ts, чтобы сделать его доступным. Сначала мы импортируем модуль TypeOrmModule и класс модели Books.
import { TypeOrmModule } from '@nestjs/typeorm';
import { Book } from './model/book';
Затем мы сделаем класс модели Book доступным с помощью метода TypeOrmModule forFeature
.
…
@Module({
imports: [TypeOrmModule.forFeature([Book])],
…
Создание представлений
Когда наша модель книги определена, давайте создадим представление для нашего приложения. Создайте папку Views в каталоге модуля books. Мы создадим шаблоны представлений для нашего приложения и будем использовать ejs, который мы установили в предыдущем разделе, в качестве шаблонизатора. Чтобы начать работу, давайте удалим шаблонный код в нашем файле main.ts и приведенный ниже код для настройки нашего шаблонизатора и каталога статических файлов.
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.useStaticAssets(join(__dirname, '..', '/src/public'));
app.setBaseViewsDir(join(__dirname, '..', '/src/books/views'));
app.setViewEngine('ejs');
await app.listen(3000);
}
bootstrap();
Теперь мы можем создать наши файлы шаблонов. Мы начнем с header.ejs и footer.ejs, которые будут созданы в папке books/views/partials. Затем создадим файлы books.ejs и book-detail.ejs в папке books/views. Откройте шаблон header.ejs и добавьте приведенный ниже фрагмент кода:
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
<title>Hello, world!</title>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand">Book Store</a>
<% if (page ==="book"){ %>
<form class="d-flex">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
/>
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#staticBackdrop"
>
Add New
</button>
<% } %>
</div>
</nav>
</body>
</html>
Наш шаблон заголовков будет выглядеть так, как показано на скриншоте ниже:
Затем откройте шаблон footer.ejs и ссылайтесь на наш файл javascript и CDN bootstrap с помощью приведенного ниже фрагмента кода:
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
<script src="/js/app.js"></script>
</body>
</html>
Представления, которые мы создали в шаблонах header.ejs и footer.ejs, будут включены в наши шаблоны books.ejs и book-detail.ejs.
Далее, наши книги будут иметь модальную HTML-форму для добавления новых книг в базу данных, а также список всех книг в нашей базе данных. В шаблоне книг у нас будет форма ввода для отправки запроса на бэкенд для сохранения книги в базе данных. Мы также включим в наш шаблон books шаблоны верхнего и нижнего колонтитулов. Откройте шаблон books.ejs и добавьте приведенный ниже фрагмент кода.
<%- include('partials/header.ejs') %>
<div class="container-fluid mt-3">
<h4>Book Store</h4>
<ol class="list-group list-group-numbered">
<% books.forEach(data=>{ %>
<li
class="list-group-item d-flex justify-content-between align-items-start"
>
<div class="ms-2 me-auto">
<div class="fw-bold">
<a href="book/<%= data.id %>"><%= data.title %></a>
</div>
</div>
<span class="badge bg-primary rounded-pill"><%= data.quantity %></span>
</li>
<% }) %>
</ol>
</div>
<!-- Modal -->
<div
class="modal fade"
id="staticBackdrop"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">Add Book</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form action="" id="createForm" method="post" action="/movie">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input
required
type="text"
class="form-control"
id="title"
placeholder="Javascript Cookbook"
name="title"
/>
</div>
<div class="mb-3">
<label for="author" class="form-label">Author</label>
<input
required
type="text"
class="form-control"
id="author"
name="author"
placeholder="Nelson Doe"
/>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">Quantity</label>
<input
required
type="number"
class="form-control"
id="quantity"
name="quantity"
placeholder="40"
/>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea
class="form-control"
id="description"
name="description"
required
rows="3"
></textarea>
</div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</div>
</div>
</div>
<%- include('partials/footer.ejs') %>
Шаблон books будет выглядеть так, как показано на скриншоте ниже:
Далее, наш шаблон book-detail будет иметь html-модальную форму для обновления книги и кнопку удаления для удаления книги из базы данных. Мы также включим в шаблон book-detail верхний и нижний колонтитулы. Затем мы динамически отобразим подробную информацию о каждой книге в нашей базе данных.
Добавьте приведенный ниже фрагмент кода в шаблон book-detail:
<%- include('partials/header.ejs') %>
<div class="container-fluid">
<table class="table">
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col">Details</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Title</td>
<td colspan="2"><%= book.title %></td>
</tr>
<tr>
<td>Author</td>
<td colspan="2"><%= book.author %></td>
</tr>
<tr>
<td>Quantity</td>
<td colspan="2"><%= book.quantity %></td>
</tr>
<tr>
<td>Description</td>
<td colspan="2"><%= book.description %></td>
</tr>
<tr>
<td colspan="2"></td>
<td>
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#update"
>
Update
</button>
<button
type="button"
class="btn btn-danger"
onclick="deleteBook('<%= book.id %>')"
>
Delete
</button>
</td>
</tr>
</tbody>
</table>
<div
class="modal fade"
id="update"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="update">Update Book</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div
class="alert alert-success alert-dismissible fade show"
role="alert"
hidden
>
<strong>Succcess!</strong> Record Updated!.
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
<form action="" id="form">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input
type="text"
class="form-control"
id="title"
name="title"
value="<%= book.title %>"
/>
</div>
<div class="mb-3">
<label for="author" class="form-label">Author</label>
<input
type="text"
class="form-control"
id="author"
name="author"
value="<%= book.author %>"
/>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">Quantity</label>
<input
type="text"
class="form-control"
id="quantity"
name="quantity"
value="<%= book.quantity %>"
/>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea
class="form-control"
id="description"
name="description"
rows="3"
>
<%= book.description %></textarea
>
</div>
<input type="hidden" class="" name="id" value="<%= book.id %>" />
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.ejs') %>
Наш шаблон book-detail будет выглядеть так, как показано на скриншоте ниже:
Теперь давайте создадим папку static files для сохранения наших статических файлов (CSS, JS и изображений). В этом учебнике мы создадим только папку JS для нашего кода javascript. Итак, создайте общую папку в каталоге src и создайте файл js/app.js в общей папке. Затем добавьте приведенный ниже фрагмент кода в файл app.js.
function updateBook() {
const createForm = document.getElementById('form');
createForm.addEventListener('submit', async (e) => {
e.preventDefault();
const id = createForm['id'].value;
await fetch(`http://localhost:3000/book/${id}`, {
method: 'Put',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: createForm['title'].value,
author: createForm['author'].value,
quantity: createForm['quantity'].value,
description: createForm['description'].value,
}),
})
.then((data) => data.json())
.then((res) => {
if (res) {
document.querySelector('.alert').removeAttribute('hidden');
setTimeout(() => {
window.location.reload();
}, 3000);
}
});
});
}
async function deleteBook(id) {
console.log(id);
await fetch(`http://localhost:3000/book/${id}`, {
method: 'DELETE',
}).then(() => (window.location.href = '/book'));
}
updateBook();
В приведенном выше фрагменте кода мы создали две функции для обновления и удаления книги из нашей базы данных. В этих функциях мы будем использовать Fetch API для отправки запроса к конечной точке бэкенда, которую мы создадим позже.
Создание контроллера
Приступим к созданию контроллеров. Создайте контроллер Nest с помощью приведенной ниже команды:
nest generate controller /books/controller/book --flat
Приведенная выше команда создаст файл controller/book.controller.ts в папке модуля books с некоторым шаблонным кодом. Откройте файл controller/book.controller.ts и импортируйте необходимые декораторы Nest, необходимые для наших маршрутов, импортируйте наш класс модели Book и класс BookService, который мы создадим позже. Затем создайте метод конструктора BookController и свяжите наш класс BookService, чтобы сделать его доступным в других методах.
import { Controller, Render, Get, Post, Put, Delete, Param, Body, Res } from '@nestjs/common';
import { Book } from '../model/book';
import { BookService } from '../service/book.service'
@Controller('book')
export class BookController {
constructor(private readonly bookService: BookService) {}
...
Далее мы создадим наш маршрут allBook, который будет слушать GET-запрос, для рендеринга нашего шаблона книги с помощью декоратора @Render со списком книг из базы данных с помощью фрагмента кода ниже:
…
@Get()
@Render('book')
async allBook(): Promise<object> {
const books = await this.bookService.getAllBook();
return { books, page: "book" }
}
…
Далее мы создадим наш маршрут createBook. Мы используем декоратор @Body для получения входных данных пользователя из тела запроса, при этом данные из тела запроса должны соответствовать схеме книги. Затем мы перенаправим пользователя на ту же страницу с помощью метода перенаправления @Res.
…
@Post()
async createBook(@Body() book: Book, @Res() res): Promise<any> {
await this.bookService.createBook(book);
return res.redirect('/book')
}
…
Далее мы создадим маршруты getBook, updateBook и deleteBook. Мы получим идентификатор книги из параметров запроса с помощью декоратора @param, а затем получим, обновим или удалим запись, используя идентификатор. В маршруте updateBook мы также используем декоратор @Body, чтобы получить данные о новой книге из тела запроса.
…
@Get(':id')
@Render('book-detail')
async getBook(@Param() params): Promise<object> {
const book = await this.bookService.getBook(params.id)
return { book, page: "detail" }
}
@Put(':id')
async updateBook(@Param() params, @Body() book: Book): Promise<Book> {
return this.bookService.updateBook(params.id, book);
}
@Delete(':id')
async deleteBook(@Param() params): Promise<Book> {
return this.bookService.deleteBook(params.id)
}
}
Создание сервиса
На этом этапе наш контроллер готов. Давайте настроим наш сервис приложения, выполнив приведенную ниже команду:
nest generate service /books/service/book --flat
Приведенная выше команда создаст файл service/book.service.ts в папке нашего модуля books. Теперь давайте создадим наши функции обработчика маршрутов в файле service. Сначала импортируем следующие зависимости:
- Injectable: Чтобы сделать наш BookService доступным в других файлах нашего проекта.
- HttpException: Для создания пользовательских HTTP-ошибок
- HttpStatus: Для отправки пользовательского кода состояния
- InjectRepository: Для инжектирования нашего класса модели книги в наш BookService.
Мы также импортируем наш класс модели книги. Сделайте это с помощью приведенного ниже фрагмента кода:
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Book } from '../model/book';
Далее мы добавим метод конструктора в наш класс BookService. Импортируйте нашу модель книги с помощью приведенного ниже фрагмента кода:
...
constructor(@InjectRepository(Book) private readonly bookRepository: Repository<Book>) { }
...
Затем мы создадим функции-обработчики getAllBooks и createBooks. Обработчик getAllBooks будет запрашивать у базы данных все книги в базе данных и возвращать их в порядке убывания нашему контроллеру. В то время как обработчик createBook создаст новую книгу с данными объекта book, используя метод save.
…
async getAllBook(): Promise<Book[]> {
return await this.bookRepository.find({ order: { createdAt: "DESC" } })
}
async createBook(book: Book): Promise<Book> {
return await this.bookRepository.save(book)
}
…
Наконец, мы создадим обработчики getBook, updateBook и deleteBook с помощью приведенного ниже фрагмента кода. Эти обработчики будут использовать id книги для получения, обновления или удаления книги из нашей базы данных.
…
async getBook(id: string): Promise<Book> {
return await this.bookRepository.findOne(id);
}
async updateBook(id: string, book: Book): Promise<Book> {
const updateBook = await this.bookRepository.update(id, book)
if (!updateBook) {
throw new HttpException('Book id not found', HttpStatus.NOT_FOUND)
}
return await this.bookRepository.findOne(id);
}
async deleteBook(id: string): Promise<any> {
if (await this.bookRepository.delete(id)) {
return null
}
throw new HttpException('Book not found', HttpStatus.NOT_FOUND)
}
Заключение
В этом учебнике вы узнали, как построить MVC-приложение NestJS с базой данных Yugabyte, создав демонстрационный проект книжного магазина. Вы узнали, что такое архитектура MVC, как настроить приложение NestJS, а также как настроить и создать базу данных Yugabyte. Для дальнейшего чтения вы также можете прочитать больше о NestJS и YugabyteDB. Для дополнительной сложности вы можете расширить приложение, защитив маршруты удаления и обновления. Что вы будете делать дальше?