👏

NestJSでRakeタスクみたいなことをしたい

2024/04/11に公開

概要

NestJSでRailsのRakeタスクのようなことをしたいと思ったので、その方法を調べてみた。
NestJSの公式ドキュメントではmain.tsにNest-Commanderを組み込む方法が紹介されているが、それをするとタスク実行専用のアプリケーションになってしまうので、main.tsにはWebサーバーとしての機能を果たしてもらいながら、同時に他の場所でタスクも実行できるようにしたい。

状況・目標設定

状況

WebアプリケーションとしてBooksControllerやBooksServiceが稼働しているとする。

目標

Nest-Commanderを使って、BooksServiceを利用したタスクをコマンドラインから実行できるようにする。

現在のアプリケーションコード

稼働中のWebアプリケーションのコードは以下のようになっているとする。

app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BooksModule } from './books/books.module';

@Module({
  imports: [BooksModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
books/books.module.ts
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';

@Module({
  controllers: [BooksController],
  providers: [BooksService],
})
export class BooksModule {}
books/books.controller.ts
import { Controller, Get } from '@nestjs/common';
import { BooksService } from './books.service';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Get()
  findAll() {
    return this.booksService.getBooks();
  }
}
books/books.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class BooksService {
  getBooks() {
    return [
      { id: 1, title: 'Book 1', author: 'Alice' },
      { id: 2, title: 'Book 2', author: 'Bob' },
      { id: 3, title: 'Book 3', author: 'Carol' },
    ];
  }
}

localhost:3000/booksにアクセスすると、レスポンスが返ってくることも確認できる。

流れ

1. Nest-Commanderをインストール

Nest-Commanderの公式ドキュメントに従い、Nest-Commanderをインストールする。

npm i nest-commander

2. Tasks Moduleを作成

タスクをまとめるためのTasksModuleを作成する。

nest g module tasks

タスクではBooksServiceを利用する予定なので、TasksModule内でBooksModuleをインポートしておく。また、BooksModule以外のModuleでもBooksServiceを利用できるように、BooksModuleでBooksServiceをエクスポートしておく。

tasks/tasks.module.ts
import { Module } from '@nestjs/common';

import { BooksModule } from 'src/books/books.module';

@Module({
  imports: [BooksModule],
})
export class TasksModule {}
books/books.module.ts
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';

@Module({
  controllers: [BooksController],
  providers: [BooksService],
  exports: [BooksService],
})
export class BooksModule {}

3. 適当にタスクを作成

今回はgetBooksの結果をコンソールに表示するタスクを作成する。
Nest-Commanderは独自のデコレータやクラスを持つので、nest gコマンドを使って雛形を利用するより、一から手動でタスクを作成したほうが分かりやすいという印象。今回はタスクだということが分かりやすいようにtask.tsでファイル名が終わるようにした。

tasks/console-books.task.ts
import { Command, CommandRunner } from 'nest-commander';
import { BooksService } from 'src/books/books.service';

@Command({ name: 'console-books', description: 'getBooksの中身を出力' })
export class ConsoleBooksCommand extends CommandRunner {
  constructor(private readonly booksService: BooksService) {
    super();
  }

  async run(): Promise<void> {
    console.log(this.booksService.getBooks());
  }
}

@Command({ name: 'console-books', description: 'getBooksの中身を出力' })のnameの部分がタスクの名前になる。コマンドラインからタスクを呼び出すときもこの名前を利用する。
依存注入はNestJSのサービスと同様にconstructorで行う。ただし、通常のサービスと異なりconstructorの中でsuperを呼び出す必要がある。
実際のタスクの処理内容はrunメソッド内に記述する。

その他にもコマンドライン引数を取る機能などがある。詳しくはNest-Commanderの公式ドキュメントを参照。

4. TasksModuleに作成したタスクを登録

tasks/tasks.module.ts
import { Module } from '@nestjs/common';

import { BooksModule } from 'src/books/books.module';
import { ConsoleBooksCommand } from './console-books.task';

@Module({
  imports: [BooksModule],
  providers: [ConsoleBooksCommand],
})
export class TasksModule {}

5. cli.tsを作成する

main.tsと同じ階層にcli.tsを作成する。
Webアプリケーションはmain.tsを起点に、タスク実行はcli.tsを起点に実行されるイメージ。

cli.ts
import { CommandFactory } from 'nest-commander';

import { TasksModule } from './tasks/tasks.module';

async function bootstrap() {
  await CommandFactory.run(TasksModule, ['log', 'debug', 'warn', 'error']);
}

bootstrap();

['log', 'debug', 'warn', 'error']の部分はログの出力レベルの設定なのでお好みに合わせてカスタマイズ。

6. ビルドして、タスクを実行

npm run build && node dist/cli.js console-books
出力結果
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [NestFactory] Starting Nest application...
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [InstanceLoader] CommandRootModule dependencies initialized +8ms
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [InstanceLoader] BooksModule dependencies initialized +0ms
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [InstanceLoader] DiscoveryModule dependencies initialized +0ms
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [InstanceLoader] TasksModule dependencies initialized +0ms
[Nest] 95301  - 04/11/2024, 11:33:04 PM     LOG [InstanceLoader] CommandRunnerModule dependencies initialized +0ms
[
  { id: 1, title: 'Book 1', author: 'Alice' },
  { id: 2, title: 'Book 2', author: 'Bob' },
  { id: 3, title: 'Book 3', author: 'Carol' }
]

実行できました。めでたし。

ソースコード

https://github.com/mshinji/nest-commander-practice

GitHubで編集を提案

Discussion