Настройка приложения на Nest.js с точками входа через rest, graphql, console и microservices

Оглавление

Введение

В этой статье мы рассмотрим как использовать 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

Для этого будем использовать следующие пакеты:

Настройка монорепозитория

Создадим проект с помощью 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