Настройка приложения на Nest.js с точками входа через rest, graphql, console и microservices
Оглавление
- Введение
- Создание монорепозитория
- Настройка REST App
- Настройка GraphQL App
- Настройка Console App
- Настройка Microservice app
Введение
В этой статье мы рассмотрим как использовать rest, graphql, console и microservice в одном проекте. Для этого мы будем использовать Nest.js.
Рассмотрим реализацию на примере приложения ping-pong
- ping будем отправлять через протокол rest, graphql, console
- pong будем выводить через слушателя microservice
В Nest.js есть два режима организации кода:
- стандартный режим
- режим монорепозитория
Для настройки всех точек входа в одном проекте, организуем код в режиме монорепозитория.
Все приложения и библиотеки находятся в папке apps
и libs
соответственно.
Мы будем использовать следующую структуру:
prisma
├── migrations
├── schema.prisma
└── seed.ts
src
├── apps
│ ├── cli
| │ └── src
| │ ├── main.ts
│ │ ├── cli.module.ts
│ │ └── ping.command.ts
│ ├── gql
| │ └── src
| │ ├── main.ts
│ │ ├── gql.module.ts
| │ └── ping
| │ ├── ping.resolver.ts
| │ ├── ping.service.ts
| │ ├── ping.module.ts
| │ ├── dto
| │ │ └── create-ping.input.ts
│ │ └── entities
│ │ └── ping.entity.ts
│ ├── rest
| │ └── src
| │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── app.controller.ts
│ │ └── app.service.ts
│ └── srv
| └── src
| ├── main.ts
│ ├── srv.module.ts
│ ├── srv.controller.ts
│ └── srv.service.ts
└── libs
├── pingproducer
│ ├── index.ts
│ ├── pingproducer.module.ts
│ ├── pingproducer.service.ts
│ └── dto
│ └── ping.dto.ts
└── pong
├── index.ts
├── pong.module.ts
├── pong.service.ts
└── dto
└── ping.dto.ts
Для этого будем использовать следующие пакеты:
- @nestjs/graphql для работы с graphql
- nest-commander для работы с cli
- @nestjs/microservices для работы с kafka
- prisma для работы с базой данных и nestjs-prisma для интеграции с nest.js
- @nestjs/swagger для генерации swagger-документации.
Настройка монорепозитория
Создадим проект с помощью nest-cli:
npm i -g @nestjs/cli
nest new rest -p yarn --directory project-name
cd project-name
sed -i 's/rest/project-name/g' package.json
Создадим приложения и библиотеки:
yarn nest g app gql
yarn nest g app cli
yarn nest g app srv
yarn nest g lib pingproducer
yarn nest g lib pong
Весь список пакетов которые мы будем использовать далее. В каждой главе будет указан список пакетов которые нужно установить для работы с конкретной технологией.
yarn add @nestjs/config @nestjs/microservices kafkajs
yarn add @nestjs/graphql @nestjs/apollo @apollo/server graphql
yarn add nest-commander
yarn add prisma --dev
yarn add @prisma/client nestjs-prisma
yarn add @nestjs/swagger
Настройка библиотеки pingproducer
Данная библиотека будет отвечать за отправку сообщений в kafka.
Настроим подключение к kafka в режиме producerOnlyMode
для отправки сообщений.
Как указано в документации
Создадим библиотеку pingproducer
с нужными зависимостями
yarn nest g lib pingproducer
yarn add @nestjs/config @nestjs/microservices kafkajs
Создадим .env
:
KAFKAJS_NO_PARTITIONER_WARNING=1
KAFKA_CLIENT_ID=project-name
KAFKA_BROKER=localhost:19092,localhost:29092,localhost:39092
Внёсем изменения в libs/producer/src/pingproducer.module.ts
:
import { Module } from '@nestjs/common';
import { PingproducerService } from './pingproducer.service';
import { ConfigModule } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
providers: [PingproducerService],
exports: [PingproducerService],
imports: [
ConfigModule.forRoot(),
ClientsModule.register([
{
name: 'KAFKA_SERVICE',
transport: Transport.KAFKA,
options: {
client: {
clientId: process.env.KAFKA_CLIENT_ID + '-ping-producer',
brokers: process.env.KAFKA_BROKER.split(','),
},
producerOnlyMode: true,
},
},
]),
],
})
export class PingproducerModule {}
Создадим dto libs/pingproducer/src/dto/ping.dto.ts
:
export class PingDto {
name: string;
}
И Добавим dto в экспорт библиотеки libs/pingproducer/src/index.ts
:
export * from './pingproducer.module';
export * from './pingproducer.service';
export * from './dto/ping.dto';
Внёсем изменения в libs/producer/src/pingproducer.service.ts
:
import { Inject, Injectable } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';
import { lastValueFrom } from 'rxjs';
import { PingDto } from './dto/ping.dto';
@Injectable()
export class PingproducerService {
constructor(
@Inject('KAFKA_SERVICE') private readonly kafkaClient: ClientKafka,
) {}
async publishPing(dto: PingDto) {
const data = JSON.stringify(dto)
const topic = 'ping'
const observable = this.kafkaClient.emit(topic, data)
await lastValueFrom(observable)
}
}
Настройка cli
Создадим приложение cli
с нужными зависимостями
yarn nest g app cli
yarn add nest-commander
Удалим стандартные файлы:
rm apps/cli/src/cli.controller.spec.ts
rm apps/cli/src/cli.controller.ts
rm apps/cli/src/cli.service.ts
Настроим apps/cli/src/main.ts
:
import { CommandFactory } from 'nest-commander'
import { CliModule } from './cli.module';
async function bootstrap() {
await CommandFactory.run(CliModule, {
errorHandler: (err) => {
console.log(err.message)
process.exit(0)
},
})
}
bootstrap()
Создадим apps/cli/src/ping.command.ts
import { Command, CommandRunner } from 'nest-commander'
import { PingDto, PingproduceService } from '@app/pingproduce'
@Command({
name: 'ping',
description: 'Send ping',
})
export class PingCommand extends CommandRunner {
constructor(private readonly pingproduceService: PingproduceService) {
super()
}
async run(): Promise<void> {
const dto = new PingDto()
dto.name = 'ping from cli'
await this.pingproduceService.publishPing(dto)
}
}
Настроим apps/cli/src/cli.module.ts
:
import { Module } from '@nestjs/common';
import { PingCommand } from './ping.command';
import { PingproduceModule } from '@app/pingproduce';
@Module({
imports: [PingproduceModule],
providers: [PingCommand],
})
export class CliModule {}
yarn start cli -- -- ping
nest start cli -- ping
Настройка graphql
Создадим приложение gql
с нужными зависимостями
yarn nest g app gql
yarn add @nestjs/graphql @nestjs/apollo @apollo/server graphql
Удалим стандартные файлы:
rm apps/gql/src/gql.controller.spec.ts
rm apps/gql/src/gql.controller.ts
rm apps/gql/src/gql.service.ts
Настроим apps/gql/src/gql.module.ts
:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql'
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
introspection: true,
includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
}),
],
})
export class GqlModule {}
Создадим entry points для ping
nest g res -p gql ping
? What transport layer do you use? GraphQL (code first)
? Would you like to generate CRUD entry points? Yes
Отредактируем apps/gql/src/ping/ping.resolver.ts
:
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { PingService } from './ping.service';
import { Ping } from './entities/ping.entity';
import { CreatePingInput } from './dto/create-ping.input';
@Resolver(() => Ping)
export class PingResolver {
constructor(private readonly pingService: PingService) {}
@Mutation(() => Boolean)
createPing(@Args('createPingInput') createPingInput: CreatePingInput) {
return this.pingService.create(createPingInput);
}
@Query(() => Ping)
ping() {
return this.pingService.getPong();
}
}
Удалим не используемые файлы:
rm apps/gql/src/ping/dto/update-ping.input.ts
Отредактируем apps/gql/src/ping/ping.service.ts
:
import { Injectable } from '@nestjs/common';
import { CreatePingInput } from './dto/create-ping.input';
import { Ping } from './entities/ping.entity';
import { PingDto, PingproducerService } from '@app/pingproducer'
@Injectable()
export class PingService {
constructor(
private readonly pingproducerService: PingproducerService,
) {}
create(createPingInput: CreatePingInput) {
const dto = new PingDto();
dto.name = createPingInput.name;
this.pingproducerService.publishPing(dto);
return true;
}
getPong() {
const entity = new Ping();
entity.name = 'pong';
return entity;
}
}
Отредактируем apps/gql/src/ping/ping.module.ts
:
import { Module } from '@nestjs/common';
import { PingService } from './ping.service';
import { PingResolver } from './ping.resolver';
import { PingproducerModule } from '@app/pingproducer'
@Module({
providers: [PingResolver, PingService],
imports: [PingproducerModule],
})
export class PingModule {}
Запустим приложение:
yarn start:dev gql
open http://localhost:3000/graphql
Отправим запрос:
mutation {
createPing(createPingInput: {name: "ping from gql"})
}
Настройка rest
Настроим приложение rest
с нужными зависимостями
yarn add @nestjs/swagger
Настроим apps/rest/src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('ping')
async getPing(): Promise<string> {
return await this.appService.getPing();
}
}
Настроим apps/rest/src/app.service.ts
import { Injectable } from '@nestjs/common';
import { PingDto, PingproducerService } from '@app/pingproducer'
@Injectable()
export class AppService {
constructor(
private readonly pingproducerService: PingproducerService,
) {}
async getPing(): Promise<string> {
const dto = new PingDto();
dto.name = 'ping from rest'
this.pingproducerService.publishPing(dto);
return 'sent ping from rest';
}
}
Настроим apps/rest/src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PingproducerModule } from '@app/pingproducer'
@Module({
imports: [PingproducerModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Настроим swagger и поменяем порт на 3001 чтобы не конфликтовать с gql
apps/rest/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Rest app')
.setDescription('The REST API description')
.setVersion('1.0')
.addTag('pingpong')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3001);
}
Запустим приложение:
yarn start:dev rest
open http://localhost:3001/api#/default/AppController_getPing
Настройка библиотеки pong
Создадим библиотеку pong
yarn nest g lib pong
Настроим libs/pong/src/pong.service.ts
:
import { Injectable } from '@nestjs/common';
import { PingDto } from './dto/ping.dto';
@Injectable()
export class PongService {
async processingPong(dto: PingDto) {
console.log('processing pong', dto.name);
}
}
Создадим libs/pong/src/dto/ping.dto.ts
export class PingDto {
name: string;
}
И Добавим dto в экспорт библиотеки libs/pong/src/index.ts
:
export * from './pong.module';
export * from './pong.service';
export * from './dto/ping.dto';
Настройка приложения srv
yarn nest g app srv
yarn add @nestjs/config @nestjs/microservices kafkajs
Настроим microservices в apps/srv/src/main.ts
:
import { NestFactory } from '@nestjs/core';
import { SrvModule } from './srv.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(SrvModule, {
transport: Transport.KAFKA,
options: {
client: {
clientId: process.env.KAFKA_CLIENT_ID + '-srv',
brokers: process.env.KAFKA_BROKER.split(','),
},
subscribe: {
fromBeginning: false,
},
consumer: {
groupId: process.env.KAFKA_CLIENT_ID + '-srv-consumer',
},
},
})
await app.listen();
}
bootstrap();
Подключим PongModule в SrvModule
Настроим apps/srv/src/srv.module.ts
:
import { Module } from '@nestjs/common';
import { SrvController } from './srv.controller';
import { SrvService } from './srv.service';
import { ConfigModule } from '@nestjs/config';
import { PongModule } from '@app/pong';
@Module({
imports: [
ConfigModule.forRoot(),
PongModule
],
controllers: [SrvController],
providers: [SrvService],
})
export class SrvModule {}
Настроим apps/srv/src/srv.controller.ts
:
import { Controller, Get } from '@nestjs/common';
import { SrvService } from './srv.service';
import { EventPattern, Payload } from '@nestjs/microservices';
import { PingDto } from '@app/pingproducer';
@Controller()
export class SrvController {
constructor(private readonly srvService: SrvService) {}
@EventPattern(process.env.KAFKA_CLIENT_ID + '-ping')
async processingPing(@Payload() dto: PingDto) {
await this.srvService.processingPing(dto);
}
}
Настроим apps/srv/src/srv.service.ts
:
import { Injectable } from '@nestjs/common';
import { PingDto, PongService } from '@app/pong';
@Injectable()
export class SrvService {
constructor(
private readonly pongService: PongService,
) {}
async processingPing(dto: PingDto) {
await this.pongService.processingPong(dto);
}
}
Настройка prismajs
yarn add prisma --dev
yarn prisma init
Создадим prisma/schema.prisma
:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Ping {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Создадим prisma/seed.ts
:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
await prisma.ping.create({
data: {
name: 'ping from seed',
},
})
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
Создадим миграции:
yarn prisma migrate dev --name init
Устаноми клиент prisma:
yarn add @prisma/client nestjs-prisma