Привет, друзья!
В данном туториале мы разработаем простой сервер на NestJS, взаимодействующий с SQLite с помощью Prisma, с административной панелью, автоматически генерируемой с помощью AdminJS, и описанием интерфейса, автоматически генерируемым с помощью Swagger. Все это будет приготовлено под соусом TypeScript.
Если вам это интересно, прошу под кат.
NestJS — это фреймворк для разработки эффективных и масштабируемых серверных приложений на Node.js. Данный фреймворк использует прогрессивный (что означает текущую версию ECMAScript) JavaScript с полной поддержкой TypeScript (использование TypeScript является опциональным) и сочетает в себе элементы объектно-ориентированного, функционального и реактивного функционального программирования.
Под капотом NestJS использует Express (по умолчанию), но также позволяет переключиться на Fastify.
- Руководство по NestJS
- Шпаргалка по Express API
- Карманная книга по TypeScript
- Шпаргалка по TypeScript
Prisma — это современное объектно-реляционное отображение (Object Relational Mapping, ORM) для Node.js и TypeScript. Проще говоря, Prisma — это инструмент, позволяющий работать с реляционными (PostgreSQL, MySQL, SQL Server, SQLite) и нереляционной (MongoDB) базами данных с помощью JavaScript или TypeScript без использования SQL, хотя такая возможность все же имеется.
AdminJS — это инструмент, позволяющий внедрять в приложение автоматически генерируемый интерфейс админки на React. Интерфейс генерируется на основе моделей БД и позволяет управлять ее содержимым.
Swagger — это инструмент, позволяющий внедрять в приложение автоматически генерируемое описание интерфейса. Интерфейс генерируется на основании маршрутов (роутов) приложения. Специальные комментарии позволяют формировать дополнительную информацию о конечных точках.
Подготовка и настройка проекта
Глобально устанавливаем NestJS CLI и создаем NestJS-проект,:
yarn global add @nestjs/cli # or npm i -g @nestjs/cli # nestjs-prisma - название проекта/директории nest new nestjs-prisma
Переходим в созданную директорию и устанавливаем Prisma в качестве зависимости для разработки:
cd nestjs-prisma yarn add -D prisma # or npm i -D prisma
Инициализируем Prisma-проект:
yarn prisma init # or npx prisma init
Выполнение данной команды приводит к генерации файла prisma/schema.prisma, определяющего подключение к БД, генератор, используемый для генерации клиента Prisma, и схему БД, а также файла .env с переменной среды окружения _DATABASEURL, значением которой является строка, используемая Prisma для подключения к БД.
Редактируем файл schema.prisma — изменяем дефолтный провайдер postgresql на sqlite:
datasource db { provider = "sqlite" url = env("DATABASE_URL") }
Обратите внимание: для работы со схемой Prisma удобно пользоваться этим расширением для VSCode.
Определяем строку подключения к БД в файле .env:
DATABASE_URL="file:./dev.db"
Обратите внимание: БД SQLite — это просто файл, для работы с ним не требуется отдельный сервер.
Наша БД будет содержать 2 таблицы: для пользователей и постов.
Определяем соответствующие модели в файле schema.prisma:
model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) authorId Int author User @relation(fields: [authorId], references: [id]) }
Обратите внимание: между таблицами существуют отношения один-ко-многим (one-to-many), т.е. одному пользователю может принадлежать несколько постов (у каждого поста должен быть автор). Также обратите внимание, что данные пользователя должны содержать, как минимум, адрес электронной почты, а данные поста, как минимум — заголовок и автора.
Выполняем миграцию:
# init - название миграции yarn prisma migrate dev --name init # or npx prisma ...
Выполнение данной команды приводит к генерации файла prisma/dev.db, содержащего БД, и файла _prisma/migrations/20220506124711init/migration.sql (у вас название директории с файлом migration.sql будет другим) с миграцией на SQL. Также запускается установка клиента Prisma. Если по какой-то причине этого не произошло, клиента необходимо установить вручную:
yarn add @prisma/client # or npm i @prisma/client
Обратите внимание: клиент Prisma устанавливается в качестве производственной зависимости.
Также обратите внимание, что установка клиента Prisma приводит к автоматическому выполнению команды prisma generate для генерации типов TypeScript для всевозможных вариаций моделей БД. При внесении каких-либо изменений в существующие модели, добавлении новых моделей и т.п. может потребоваться выполнить эту команду вручную для обновления клиента (приведения его в соответствие с БД).
На этом подготовка и настройка проекта завершены и можно приступать к разработке REST API.
Разработка REST API
При разработке REST API, подключении AdminJS и Swagger мы будем работать с файлами, находящими в директории src.
Начнем с создания PrismaService, отвечающего за инстанцирование (создание экземпляра) PrismaClient и подключение к БД (а также отключение от нее). Создаем файл prisma.service.ts следующего содержания:
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { // подключаемся к БД при инициализации модуля await this.$connect(); } async enableShutdownHooks(app: INestApplication) { this.$on('beforeExit', async () => { // закрываем приложение при отключении от БД await app.close(); }); } }
Теперь займемся сервисами для обращения к БД с помощью моделей User и Post из схемы Prisma.
Создаем файл user.service.ts следующего содержания:
import { Injectable } from '@nestjs/common'; // преимущество использования `Prisma` в `TypeScript-проекте` состоит в том, // что `Prisma` автоматически генерирует типы для моделей и их вариаций import { User, Prisma } from '@prisma/client'; import { PrismaService } from './prisma.service'; @Injectable() export class UserService { // внедряем зависимость constructor(private prisma: PrismaService) {} // получение пользователя по email async user(where: Prisma.UserWhereUniqueInput): Promise<User | null> { return this.prisma.user.findUnique({ where, }); } // получение всех пользователей async users(params: { skip?: number; take?: number; cursor?: Prisma.UserWhereUniqueInput; where?: Prisma.UserWhereInput; orderBy?: Prisma.UserOrderByWithRelationInput; }): Promise<User[]> { const { skip, take, cursor, where, orderBy } = params; return this.prisma.user.findMany({ skip, take, cursor, where, orderBy, }); } // создание пользователя async createUser(data: Prisma.UserCreateInput): Promise<User> { return this.prisma.user.create({ data }); } // обновление пользователя async updateUser(params: { where: Prisma.UserWhereUniqueInput; data: Prisma.UserUpdateInput; }): Promise<User> { const { where, data } = params; return this.prisma.user.update({ data, where, }); } // удаление пользователя async removeUser(where: Prisma.UserWhereUniqueInput): Promise<User> { return this.prisma.user.delete({ where }); } }
Создаем файл post.service.ts следующего содержания:
import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { Post, Prisma } from '@prisma/client'; type GetPostsParams = { skip?: number; take?: number; cursor?: Prisma.PostWhereUniqueInput; where?: Prisma.PostWhereInput; orderBy?: Prisma.PostOrderByWithRelationInput; }; @Injectable() export class PostService { constructor(private prisma: PrismaService) {} // получение поста по id async post(where: Prisma.PostWhereUniqueInput): Promise<Post | null> { return this.prisma.post.findUnique({ where }); } // получение всех постов async posts(params: GetPostsParams) { return this.prisma.post.findMany(params); } // создание поста async createPost(data: Prisma.PostCreateInput): Promise<Post> { return this.prisma.post.create({ data }); } // обновление поста async updatePost(params: { where: Prisma.PostWhereUniqueInput; data: Prisma.PostUpdateInput; }): Promise<Post> { return this.prisma.post.update(params); } // удаление поста async removePost(where: Prisma.PostWhereUniqueInput): Promise<Post> { return this.prisma.post.delete({ where }); } }
Определим несколько роутов в основном контроллере приложения. Редактируем файл app.controller.ts:
import { Controller, Get, Param, Post, Body, Put, Delete, } from '@nestjs/common'; import { UserService } from './user.service'; import { PostService } from './post.service'; import { User as UserModel, Post as PostModel } from '@prisma/client'; type UserData = { email: string; name?: string }; type PostData = { title: string; content?: string; authorEmail: string; }; // добавляем префикс пути @Controller('api') export class AppController { constructor( // внедряем зависимости private readonly userService: UserService, private readonly postService: PostService, ) {} @Get('post/:id') async getPostById(@Param('id') id: string): Promise<PostModel> { return this.postService.post({ id: Number(id) }); } @Get('feed') async getPublishedPosts(): Promise<PostModel[]> { return this.postService.posts({ where: { published: true, }, }); } @Get('filtered-posts/:searchString') async getFilteredPosts( @Param('searchString') searchString: string, ): Promise<PostModel[]> { return this.postService.posts({ where: { OR: [ { title: { contains: searchString }, }, { content: { contains: searchString }, }, ], }, }); } @Post('post') async createDraft(@Body() postData: PostData): Promise<PostModel> { const { title, content, authorEmail } = postData; return this.postService.createPost({ title, content, author: { connect: { email: authorEmail }, }, }); } @Put('publish/:id') async publishPost(@Param('id') id: string): Promise<PostModel> { return this.postService.updatePost({ where: { id: Number(id) }, data: { published: true }, }); } @Delete('post/:id') async removePost(@Param('id') id: string): Promise<PostModel> { return this.postService.removePost({ id: Number(id) }); } @Post('user') async registerUser(@Body() userData: UserData): Promise<UserModel> { return this.userService.createUser(userData); } }
Контроллер реализует следующие роуты:
- GET:
- /post/:id: получение поста по id;
- /feed: получение всех опубликованных постов;
- filtered-posts/:searchString: получение постов, отфильтрованных по заголовку или содержимому;
- POST:
- /post: создание поста:
- тело запроса:
- title: String (обязательно): заголовок;
- content: String (опционально): содержимое;
- authorEmail: String (обязательно): email автора;
- /user: создание пользователя:
- тело запроса:
- email: String (обязательно): адрес электронной почты;
- name: String (опционально): имя;
- PUT:
- /publish/:id: публикация поста по id;
- DELETE:
- /post/:id: удаление поста по id.
Обратите внимание: ко всем роутам будет автоматически добавлен префикс пути, определенный в контроллере (api). Также обратите внимание, что в реальном приложении большинство (если не все) роуты, связанные с постами, будут защищенными (private), т.е. доступными только зарегистрированным и авторизованным пользователям (выполняющим запрос с токеном доступа — access token).
Внедряем провайдеры в основной модуль приложения (app.module.ts):
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; // ! import { PrismaService } from './prisma.service'; import { UserService } from './user.service'; import { PostService } from './post.service'; @Module({ imports: [], controllers: [AppController], // ! providers: [PrismaService, UserService, PostService], }) export class AppModule {}
Для корректной работы Prisma с enableShutdownHooks требуется немного отредактировать файл main.ts:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { PrismaService } from './prisma.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); // ! const prismaService = app.get(PrismaService); await prismaService.enableShutdownHooks(app); await app.listen(3000); } bootstrap();
На этом разработка REST API завершена. Давайте убедимся в работоспособности сервера. Для этого я буду использовать Insomnia.
Запускаем сервер в режиме для разработки:
yarn start:dev # or npm run start:dev
Сам сервер доступен по адресу http://localhost:3000, а определенный нами REST API по адресу http://localhost:3000/api.
Регистрируем нового пользователя с именем Bob и адресом электронной почты bob@email.com:
Создаем от имени Bob 3 поста:


Получаем пост с id, равным 4:
Публикуем посты с id, равными 5 и 6:

Получаем опубликованные посты:
Получаем посты, в заголовке или содержимом которых встречается слово title2 (независимо от регистра):
Удаляем пост с id, равным 5:
Получаем посты, в которых встречается слово title (в нашем случае, все посты):
Отлично, сервер работает, как ожидается.
Приступим к внедрению в приложение админки.
Внедрение админки
Обратите внимание: модули AdminJS для работы с NestJS и Prisma являются экспериментальными, т.е. находятся в стадии активной разработки. Это означает, что способ их подключения и использования в будущем может измениться.
Устанавливаем зависимости:
yarn add adminjs @adminjs/nestjs express @adminjs/express express-formidable express-session # or npm i ...
Обратите внимание: несмотря на то, что Express является зависимостью NestJS (поскольку используется в качестве дефолтной нижележащей платформы — underlying platform), для корректной работы AdminJS он должен быть установлен в качестве производственной зависимости приложения. Также обратите внимание, что согласно документации AdminJS, установка пакета express-session является опциональной, но на сегодняшний день это не так: без него @adminjs/express категорически отказывается от сотрудничества, а без @adminjs/express не работает @adminjs/nestjs.
Оформим код AdminJS в виде отдельного модуля. Создаем файл admin.module.ts следующего содержания:
import AdminJS from 'adminjs'; // без этого `@adminjs/nestjs` по какой-то причине "не видит" `@aminjs/express`, необходимый ему для работы import '@adminjs/express'; import { AdminModule } from '@adminjs/nestjs'; import { Database, Resource } from '@adminjs/prisma'; // мы не можем использовать `User` и `Post` из `@prisma/client`, // поскольку нам нужны модели, а не типы, // поэтому приходится делать так import { PrismaClient } from '@prisma/client'; import { DMMFClass } from '@prisma/client/runtime'; 1; const prisma = new PrismaClient(); const dmmf = (prisma as any)._dmmf as DMMFClass; AdminJS.registerAdapter({ Database, Resource }); export default AdminModule.createAdmin({ adminJsOptions: { // путь к админке rootPath: '/admin', // в этом списке должны быть указаны все модели/таблицы БД, // доступные для редактирования resources: [ { resource: { model: dmmf.modelMap.User, client: prisma }, }, { resource: { model: dmmf.modelMap.Post, client: prisma }, }, ], }, });
Подключаем (импортируем) этот модуль в AppModule:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { PrismaService } from './prisma.service'; import { UserService } from './user.service'; import { PostService } from './post.service'; // ! import AdminModule from './admin.module'; @Module({ // ! imports: [AdminModule], controllers: [AppController], providers: [PrismaService, UserService, PostService], }) export class AppModule {}
Перезапускаем сервер и переходим по адресу http://localhost:3000/admin:
Изучим содержимое таблицы постов. Для этого нажимаем на Post на панели навигации слева:
Стандартный интерфейс админки позволяет создавать новые записи, редактировать и удалять существующие, а также фильтровать записи по полям.
Редактируем запись с id, равным 4: изменяем заголовок на Title2, содержимое на Content2 и публикуем пост. Удаляем запись с id === 6 и создаем запись с заголовком Title4 и содержимым Content4:
Возвращаемся в Insomnia и получаем все посты (в которых встречается слово title):
Как видим, выполненные в админке операции привели к обновлению данных в базе.
Обратите внимание: если функционал вашей админки будет ограничен редактированием записей в БД, лучше воспользоваться решением, предоставляемым Prisma, что называется, из коробки. Речь идет о Prisma Studio.
Запускаем Prisma Studio с помощью следующей команды:
yarn prisma studio # or npx prisma studio
Переходим по адресу http://localhost:5555:
Prisma Studio предназначен исключительно для редактирования записей в БД. AdminJS предоставляет более широкие возможности по работе с данными и не только.
На этом разработка админки завершена.
Приступим к внедрению в приложение документации.
Внедрение документации
С внедрением в приложение документации все гораздо проще, поскольку NestJS поддерживает Swagger (Open API) из коробки.
Устанавливаем зависимости:
yarn add @nestjs/swagger swagger-ui-express # or npm i ...
Подключаем Swagger в основном файле приложения (main.ts):
import { NestFactory } from '@nestjs/core'; // swagger import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { PrismaService } from './prisma.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); // prisma const prismaService = app.get(PrismaService); await prismaService.enableShutdownHooks(app); // swagger const config = new DocumentBuilder() // заголовок .setTitle('Title') // описание .setDescription('Description') // версия .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, config); // первый параметр - префикс пути, по которому будет доступна документация SwaggerModule.setup('swagger', app, document); await app.listen(3000); } bootstrap();
Перезапускаем сервер и переходим по адресу http://localhost:3000/swagger:
Как видим, Swagger успешно разрешил (обнаружил и проанализировал) все роуты нашего приложения. Форму (shape) ответов и другую дополнительную информацию о маршрутах можно определить вручную с помощью специальных комментариев.
Для того, чтобы получить сгенерированные Swagger данные в виде JSON-объекта следует перейти по адресу http://localhost:3000/swagger-json:
Подробнее о поддержке NestJS спецификации Open API можно почитать здесь.
Таким образом, нам удалось минимальными усилиями реализовать относительно полноценный и полностью типизированный («типобезопасный» — type safe) REST API с автоматически генерируемой админкой и документацией.
Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Надеюсь, вы нашли для себя что-то интересное и не зря потратили время.
Благодарю за внимание и happy coding!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/665794/

Добавить комментарий