💭

今更ながら、NestJSのCLIをちゃんと理解する!

に公開

今更ながら、NestJSのCLIをちゃんと理解する!

本記事のサマリ

NestJS CLIには、プロジェクト初期化以外にも開発効率を大幅に向上させる機能が数多く存在します。特にGraphQLプロジェクトでは、nest generate resourceによる一括生成、CLI Pluginによる自動アノテーション、そしてmonorepo機能によるコード共有が強力な武器となります。本記事では、これらの機能を実際の開発現場でどう活用するかを具体例とともに解説します。

始めに:なぜCLIをもっと活用したいのか

GraphQLを使ったNestJSプロジェクトを開発していると、同じようなパターンのコードを何度も書くことになりませんか?新しいリソースを追加するたびに、Resolver、Service、DTO、Entityを手作業で作成して、しかも @Fieldデコレータを一つずつ付けて...。正直、面倒ですよね。

私も以前は、こうした繰り返し作業にうんざりして、Hygenのようなカスタムテンプレート生成ツールに頼っていました。確かに自分なりのテンプレートを用意すれば、プロジェクト固有の構造に合わせてファイルを生成できます。でも、NestJS CLIの機能をちゃんと理解してみると、「あれ、これで十分じゃないか?」と思える場面が意外と多いんです。

今回は、プロジェクト初期化(nest new)しか使ったことがない人向けに、NestJS CLIの隠れた便利機能を3つピックアップして紹介します。特にGraphQLプロジェクトで威力を発揮するものを中心に、実際の開発でどう使えるかをお話しします。

NestJS CLIの基本的な立ち位置

まず、NestJS CLIが何をしてくれるツールなのか簡単におさらいしておきましょう。

https://docs.nestjs.com/cli/overview

NestJS CLIは、単なるプロジェクト生成ツールではありません。開発からビルド、テストまでの開発ライフサイクル全体をサポートする包括的なツールです。TypeScriptのコンパイル、Webpackバンドリング、ファイル監視など、開発に必要な機能がすべて統合されています。

コマンドは大きく分けて以下のような種類があります:

  • nest new: プロジェクト作成
  • nest generate: コンポーネント生成
  • nest add: ライブラリ追加
  • nest info: システム情報表示

この中で、多くの開発者が nest newしか使っていないのは、もったいないというのが正直な感想です。

特に注目すべき3つの機能

1. nest generate resource: CRUD生成の決定版

最初に紹介したいのが、nest generate resource(短縮形は nest g res)です。これは、新しいリソースを追加する際の救世主的なコマンドです。

https://docs.nestjs.com/recipes/crud-generator

実際に使ってみましょう:

$ nest g resource users

このコマンドを実行すると、まず何のタイプのプロジェクトかを聞かれます:

? What transport layer do you use? (Use arrow keys)
❯ REST API
  GraphQL (code first)
  GraphQL (schema first) 
  Microservice (non-HTTP)
  WebSocket Gateway

GraphQL(code first)を選択すると、次にCRUDエンドポイントを生成するか聞かれます。「Yes」を選ぶと、以下のファイルが一気に生成されます:

? What transport layer do you use? GraphQL (code first)
? Would you like to generate CRUD entry points? Yes
CREATE src/nekos/nekos.module.ts (225 bytes)
CREATE src/nekos/nekos.resolver.spec.ts (525 bytes)
CREATE src/nekos/nekos.resolver.ts (1109 bytes)
CREATE src/nekos/nekos.service.spec.ts (453 bytes)
CREATE src/nekos/nekos.service.ts (625 bytes)
CREATE src/nekos/dto/create-neko.input.ts (196 bytes)
CREATE src/nekos/dto/update-neko.input.ts (243 bytes)
CREATE src/nekos/entities/neko.entity.ts (187 bytes)
UPDATE src/app.module.ts (671 bytes)

ちなみに、GraphQL(schema first)を選んだ場合は、.graphqlファイルも生成されます:

? What transport layer do you use? GraphQL (schema first)
? Would you like to generate CRUD entry points? Yes
CREATE src/inus/inus.graphql (396 bytes)
CREATE src/inus/inus.module.ts (218 bytes)
CREATE src/inus/inus.resolver.spec.ts (515 bytes)
CREATE src/inus/inus.resolver.ts (948 bytes)
CREATE src/inus/inus.service.spec.ts (446 bytes)
CREATE src/inus/inus.service.ts (623 bytes)
CREATE src/inus/dto/create-inus.input.ts (32 bytes)
CREATE src/inus/dto/update-inus.input.ts (192 bytes)
CREATE src/inus/entities/inus.entity.ts (21 bytes)
UPDATE package.json (1199 bytes)
UPDATE src/app.module.ts (736 bytes)

code firstとschema firstの違い

項目 code first schema first
スキーマの管理 TypeScriptから自動生成 .graphqlファイルを手書き
エントリポイント DTOやEntityが中心 スキーマファイルが中心
型の定義 TypeScriptが中心で便利 GraphQLタイプとTS型を対応させる手間あり
用途例 内製バックエンドのAPIで便利 公開APIや外部仕様を尊重する場合に便利

生成されるResolverのコードにも明確な違いがあります。

code first(neko.resolver.ts)の例:

@Resolver(() => Neko)
export class NekosResolver {
  constructor(private readonly nekosService: NekosService) {}

  @Mutation(() => Neko)
  createNeko(@Args('createNekoInput') createNekoInput: CreateNekoInput) {
    return this.nekosService.create(createNekoInput);
  }

  @Query(() => [Neko], { name: 'nekos' })
  findAll() {
    return this.nekosService.findAll();
  }

  @Query(() => Neko, { name: 'neko' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.nekosService.findOne(id);
  }
}

schema first(inus.resolver.ts)の例:

@Resolver('Inus')
export class InusResolver {
  constructor(private readonly inusService: InusService) {}

  @Mutation('createInus')
  create(@Args('createInusInput') createInusInput: CreateInusInput) {
    return this.inusService.create(createInusInput);
  }

  @Query('inus')
  findAll() {
    return this.inusService.findAll();
  }

  @Query('inus')
  findOne(@Args('id') id: number) {
    return this.inusService.findOne(id);
  }
}

違いのポイント:

  • code first: デコレータに型情報を渡す(@Resolver(() => Neko)@Query(() => [Neko])
  • schema first: デコレータに文字列を渡す(@Resolver('Inus')@Query('inus')
  • code first: 型安全性が高く、IDEの補完も効きやすい
  • schema first: GraphQLスキーマファイルとの整合性を手動で管理する必要がある

多くの内製プロジェクトではcode firstの方が開発効率が高く、型安全性の面でも優れています。一方、既存のGraphQLスキーマ定義がある場合や、フロントエンドチームとスキーマ定義を先に合意したい場合はschema firstが適しています。

生成されるServiceクラスも、すぐに開発を始められる形になっています:

@Injectable()
export class NekosService {
  create(createNekoInput: CreateNekoInput) {
    return 'This action adds a new neko';
  }

  findAll() {
    return `This action returns all nekos`;
  }

  findOne(id: number) {
    return `This action returns a #${id} neko`;
  }

  update(id: number, updateNekoInput: UpdateNekoInput) {
    return `This action updates a #${id} neko`;
  }

  remove(id: number) {
    return `This action removes a #${id} neko`;
  }
}

各メソッドには仮の実装が入っているため、すぐにGraphQL Playgroundで動作確認ができます。実際のデータベース接続やビジネスロジックは、この土台の上に追加していく形です。

Query、Mutationが適切に定義されており、DTOクラスも生成済み。さらに、App Moduleへの登録まで自動で行われます。手作業だと5〜10分はかかる作業が、文字通り1分で完了します。

ここで重要なのは、生成されるコードがNestJSのベストプラクティスに沿っていることです。ファイル名の命名規則、フォルダ構造、インポート文の書き方まで、すべて一貫性があります。

2. CLI Plugin: 自動アノテーションの魔法

2つ目は、CLI Plugin機能です。これは、コンパイル時にTypeScriptのAST(抽象構文木)を解析して、必要なデコレータを自動で付与してくれる機能です。

https://docs.nestjs.com/graphql/cli-plugin

GraphQLプロジェクトでは、通常このようなコードを書く必要があります:

@ObjectType()
export class User {
  @Field(() => ID)
  id: number;

  @Field({ nullable: true })
  firstName?: string;

  @Field({ nullable: true })
  lastName?: string;

  @Field(() => [Post])
  posts: Post[];
}

しかし、CLI Pluginを有効にすると、これが以下のように書けます:

@ObjectType()
export class User {
  @Field(() => ID)
  id: number;
  firstName?: string;
  lastName?: string;
  posts: Post[];
}

@Fieldデコレータの大部分が不要になります。プラグインがTypeScriptの型情報から自動的に判断して、適切なデコレータを付与してくれるからです。

設定方法は、nest-cli.jsonに以下を追加するだけです:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": ["@nestjs/graphql"]
  }
}

さらに、introspectCommentsオプションを有効にすると、コメントからドキュメントを自動生成してくれます:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/graphql",
        "options": {
          "introspectComments": true
        }
      }
    ]
  }
}

この設定により、以下のようなコメントが:

/**
 * ユーザーの役割一覧
 * @example ['admin', 'user']
 */
roles: string[];

自動的にGraphQLスキーマのdescriptionとexampleに変換されます。ドキュメントの重複記述が不要になり、保守性が向上します。

3. Monorepo/Library: コード共有の新しい形

3つ目は、monorepo機能とlibrary機能です。複数のアプリケーション間でコードを共有したい場合、従来はnpmパッケージにするか、Git submoduleを使うか、といった選択肢がありました。しかし、NestJS CLIのmonorepo機能を使えば、もっとスマートに解決できます。

https://docs.nestjs.com/cli/monorepo

まず、既存のプロジェクトをmonorepoに変換します:

$ nest generate app my-admin-app

この時点で、プロジェクト構造が変わります:

apps/
  my-project/     # 元のアプリケーション
  my-admin-app/   # 新しいアプリケーション
libs/             # 共有ライブラリ用フォルダ
nest-cli.json
package.json

次に、共有したい機能をライブラリとして切り出します:

$ nest generate library shared-auth

https://docs.nestjs.com/cli/libraries

これで、libs/shared-auth/以下に共有ライブラリが作成されます。このライブラリは、複数のアプリケーションから以下のようにインポートできます:

import { SharedAuthModule } from '@app/shared-auth';

@Module({
  imports: [SharedAuthModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@app/shared-authという記法は、TypeScriptのpath mappingによって実現されています。nest-cli.jsonの更新と tsconfig.jsonのpaths設定が自動で行われるため、開発者は意識することなく共有ライブラリを利用できます。

この仕組みの素晴らしい点は、ライブラリを変更したら即座に全てのアプリケーションに反映されることです。npmパッケージのようにpublish/installのサイクルが不要で、開発効率が大幅に向上します。

その他の便利なコマンド

上記の3つ以外にも、知っておくと便利なコマンドがあります。

nest addは、NestJS用にパッケージされたライブラリを簡単にインストールできます。通常の npm installと違い、パッケージのインストールに加えて、そのライブラリが提供する「install schematic(インストールスキーマティック)」を実行してくれます。

例えば、nestjs-prismaをインストールする場合:

$ nest add nestjs-prisma

このコマンドは以下のような作業を自動で行います:

  • Prismaの初期化(npx prisma init
  • package.jsonへのPrisma用npm scriptsの追加
  • シード用スクリプトの作成
  • PrismaServicePrismaModuleの生成
  • Dockerfiledocker-compose.ymlの作成(オプション)
  • tsconfig.build.jsonでのprismaディレクトリ除外設定

https://nestjs-prisma.dev/docs/schematics/

別の例として、Azure Functions対応にする場合も同様です:

$ nest add @nestjs/azure-func-http

これで以下のファイルが自動生成されます:

CREATE /.funcignore
CREATE /host.json
CREATE /local.settings.json
CREATE /proxies.json

https://github.com/nestjs/azure-func-http

つまり、nest addは単なるパッケージインストーラーではなく、「そのライブラリを使うために必要な初期セットアップを全部やってくれるツール」なのです。ただし、すべてのnpmパッケージが nest addに対応しているわけではなく、NestJS用に特別にパッケージングされたライブラリのみが対応しています。

nest build --watchnest start --debugといったオプションも、開発時の生産性を向上させてくれます。特にデバッグモードは、VSCodeなどでブレークポイントを使ったデバッグが可能になります。

Hygenとの使い分け:どちらを使うべきか

さて、冒頭で触れたHygenとの使い分けについて考えてみましょう。

NestJS CLIが優れているのは、NestJSのエコシステム内での一貫性と、メンテナンスコストの低さです。公式が提供する機能なので、NestJSのアップデートに追従してくれますし、コミュニティでの情報共有も活発です。

一方、Hygenが有効なのは、プロジェクト固有の複雑な要件がある場合です。例えば:

  • 特定のディレクトリ構造に合わせたい
  • プロジェクト固有のコメントテンプレートを含めたい
  • 複数のファイル種類を横断した生成を行いたい

私の推奨する使い分けは以下の通りです:

  1. 初期生成: NestJS CLIの nest generate resourceを使用
  2. 微調整: 生成されたファイルを手動で調整
  3. 複雑な要件: Hygenでカスタムテンプレートを作成

つまり、「まずはNestJS CLIで試してみて、足りない部分をHygenで補完する」というアプローチです。多くの場合、NestJS CLIで生成したコードを少し調整するだけで十分なケースが多いと感じています。

まとめ:CLIを味方につけましょう!

NestJS CLIは、プロジェクト初期化だけでなく、日々の開発作業を大幅に効率化してくれるツールです。特に今回紹介した3つの機能は、GraphQLプロジェクトでの開発体験を劇的に改善してくれます。

  • nest generate resourceで初期コードを一括生成
  • CLI Pluginで面倒なデコレータ記述を自動化
  • monorepo/library機能でコード共有を簡単に

もちろん、プロジェクトの要件によってはHygenのようなツールの方が適している場合もあります。でも、「NestJS CLIでできることは何か」を一度整理してから判断することで、より良い開発環境を構築できるのではないでしょうか。

CLI機能を使いこなすことで、コーディング以外の時間を削減し、より本質的な開発作業に集中できるようになります。まだ nest newしか使ったことがない方は、ぜひ一度試してみてください。きっと、「もっと早く知りたかった」と思うはずです!

株式会社StellarCreate | Tech blog📚

Discussion