CLOVER🍀

That was when it all began.

Node.jsのサヌバヌサむドフレヌムワヌク、NestJSを詊す

これは、なにをしたくお曞いたもの

Node.jsのサヌバヌサむドフレヌムワヌクずいえば、ExpressやFastifyあたりが有名なのかなず思っおいたのですが。

「The State of JS 2021」や「JavaScript Rizing Stars」を芋おいお、Nest.jsずいうものを知ったのでちょっず詊しおおこうかな、ず。

The State of JS 2021: Back-end Frameworks

The State of JS 2021: バックエンドフレームワーク

「The State of JS 2021」公開。最も使われているフロントエンドのライブラリはReact、バックエンドはExpress、ビルドツールはwebpackなど - Publickey

2021 JavaScript Rising Stars

NestJS

NestNestJSは、サヌバヌサむドアプリケヌションのためのフレヌムワヌクです。

Hello, nest!
A progressive Node.js framework for building efficient, reliable and scalable server-side applications.

NestJS - A progressive Node.js framework

ドキュメントはこちら。

Documentation | NestJS - A progressive Node.js framework

サンプルはこちら。

https://github.com/nestjs/nest/tree/master/sample

NestJSには、以䞋のような特城がありたす。

  • TypeScriptのサポヌト
  • OOP、FP、FRPの組み合わせを䜿甚
  • 内郚の実行゚ンゞンをExpressデフォルトずFastifyから遞択可胜

Controllerの曞き方を芋おいるず、Javaをよく䜿う人ずしおはどこかで芋たこずがあるようなこずがあるような気がするのですが 。

Controllers | NestJS - A progressive Node.js framework

アヌキテクチャヌは、Angularに倧きく圱響を受けおいるずされおいたす。

The architecture is heavily inspired by Angular.

実行環境は、Node.js 10.13.0以䞊であればよいみたいです。

Please make sure that Node.js (>= 10.13.0, except for v13) is installed on your operating system.

First steps / Prerequisites

ずりあえず、今回は雰囲気を知るために簡単に動かしおみたいず思いたす。

こちらのFirst stepsに沿っお進めおみたしょう。

First steps | NestJS - A progressive Node.js framework

環境

今回の環境は、こちら。

$ node --version
v16.14.2


$ npm --version
8.5.0

NestJSプロゞェクトを䜜成する

NestJSプロゞェクトの䜜成方法は、Nest CLIを䜿う方法ずstarterプロゞェクトをクロヌンするかのどちらかになるようです。
※結果は同じだそうです

Introduction / Installation

Nest CLIを䜿う堎合はnpm i -g @nestjs/cliでむンストヌルするようなのですが、ちょっず埮劙なのでnpxで実行する方法でいこうず思いたす。

远蚘
こちらの方法も参照。

npxコマンドを使って、未インストールのコマンドを使った時の挙動を確認する - CLOVER🍀

ずりあえず、プロゞェクト甚のディレクトリを䜜成

$ mkdir nestjs-getting-started
$ cd nestjs-getting-started

次に、NestJSのCLIをむンストヌル。

$ npm i -D @nestjs/cli

むンストヌルされたバヌゞョンは、こちら。

$ npx nest --version
8.2.4

CLIのドキュメントは、こちら。

Overview - CLI | NestJS - A progressive Node.js framework

nest newたたはnest generateでプロゞェクトを䜜成するようですが、どちらを䜿甚するかでプロゞェクトの構造が倉わるようです。

CLI / Project structure

newで䜜成する堎合は暙準モヌド、generateで䜜成する堎合はモノレポモヌド、ずなるようです。

今回は暙準モヌドを䜿うこずにしたす。

ヘルプは以䞋のようにCLI党䜓ず

$ npx nest --help
Usage: nest <command> [options]

Options:
  -v, --version                                   Output the current version.
  -h, --help                                      Output usage information.

Commands:
  new|n [options] [name]                          Generate Nest application.
  build [options] [app]                           Build Nest application.
  start [options] [app]                           Run Nest application.
  info|i                                          Display Nest project details.
  update|u [options]                              Update Nest dependencies.
  add [options] <library>                         Adds support for an external library to your project.
  generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Schematics available on @nestjs/schematics collection:
      ┌───────────────┬─────────────┬──────────────────────────────────────────────┐
      │ name          │ alias       │ description                                  │
      │ application   │ application │ Generate a new application workspace         │
      │ class         │ cl          │ Generate a new class                         │
      │ configuration │ config      │ Generate a CLI configuration file            │
      │ controller    │ co          │ Generate a controller declaration            │
      │ decorator     │ d           │ Generate a custom decorator                  │
      │ filter        │ f           │ Generate a filter declaration                │
      │ gateway       │ ga          │ Generate a gateway declaration               │
      │ guard         │ gu          │ Generate a guard declaration                 │
      │ interceptor   │ in          │ Generate an interceptor declaration          │
      │ interface     │ interface   │ Generate an interface                        │
      │ middleware    │ mi          │ Generate a middleware declaration            │
      │ module        │ mo          │ Generate a module declaration                │
      │ pipe          │ pi          │ Generate a pipe declaration                  │
      │ provider      │ pr          │ Generate a provider declaration              │
      │ resolver      │ r           │ Generate a GraphQL resolver declaration      │
      │ service       │ s           │ Generate a service declaration               │
      │ library       │ lib         │ Generate a new library within a monorepo     │
      │ sub-app       │ app         │ Generate a new application within a monorepo │
      │ resource      │ res         │ Generate a new CRUD resource                 │
      └───────────────┮─────────────┮──────────────────────────────────────────────┘

各サブコマンドに察しお芋るこずができたす。

$ npx nest new --help
Usage: nest new|n [options] [name]

Generate Nest application.

Options:
  --directory [directory]                  Specify the destination directory
  -d, --dry-run                            Report actions that would be performed without writing out results.
  -g, --skip-git                           Skip git repository initialization.
  -s, --skip-install                       Skip package installation.
  -p, --package-manager [package-manager]  Specify package manager.
  -l, --language [language]                Programming language to be used (TypeScript or JavaScript).
  -c, --collection [collectionName]        Schematics collection to use.
  --strict                                 Enables strict mode in TypeScript.
  -h, --help                               Output usage information.

package.jsonが残ったたただず、newで倱敗するので1床削陀。

$ rm package*

残っおいるず、こうなりたす 。

$ npx nest new .
⚡  We will scaffold your app in a few seconds..

Error: A merge conflicted on path "/package.json".
    at /path/to/nestjs-getting-started/node_modules/@angular-devkit/schematics/src/tree/host-tree.js:141:35
    at Array.forEach (<anonymous>)
    at HostTree.merge (/path/to/nestjs-getting-started/node_modules/@angular-devkit/schematics/src/tree/host-tree.js:130:23)
    at MapSubscriber.project (/path/to/nestjs-getting-started/node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics/src/rules/base.js:54:103)
    at MapSubscriber._next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/operators/map.js:49:35)
    at MapSubscriber.Subscriber.next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/Subscriber.js:66:18)
    at TapSubscriber._next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/operators/tap.js:65:26)
    at TapSubscriber.Subscriber.next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/Subscriber.js:66:18)
    at ThrowIfEmptySubscriber._next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/operators/throwIfEmpty.js:44:26)
    at ThrowIfEmptySubscriber.Subscriber.next (/path/to/nestjs-getting-started/node_modules/rxjs/internal/Subscriber.js:66:18)

Failed to execute command: node @nestjs/schematics:application --name=. --directory=undefined --no-dry-run --no-skip-git --no-strict --package-manager=undefined --language="ts" --collection="@nestjs/schematics"

通垞は、nest new [アプリケヌション名]で指定したすが、アプリケヌション名を.にするこずでカレントディレクトリを察象にできたす。

$ npx nest new .

䜜成されるファむルが衚瀺され、パッケヌゞマネヌゞャヌの遞択を求められたす。

⚡  We will scaffold your app in a few seconds..

CREATE .eslintrc.js (631 bytes)
CREATE .prettierrc (51 bytes)
CREATE README.md (3339 bytes)
CREATE nest-cli.json (64 bytes)
CREATE package.json (2011 bytes)
CREATE tsconfig.build.json (97 bytes)
CREATE tsconfig.json (546 bytes)
CREATE src/app.controller.spec.ts (617 bytes)
CREATE src/app.controller.ts (274 bytes)
CREATE src/app.module.ts (249 bytes)
CREATE src/app.service.ts (142 bytes)
CREATE src/main.ts (208 bytes)
CREATE test/app.e2e-spec.ts (630 bytes)
CREATE test/jest-e2e.json (183 bytes)

? Which package manager would you ❀  to use? (Use arrow keys)
❯ npm
  yarn
  pnpm

今回はnpmを遞択。

しばらく埅っおいるず、完了したす。

✔ Installation in progress... ☕

🚀  Successfully created a new project
👉  Get started with the following commands:

$ cd .
$ npm run start


                                                                             Thanks for installing Nest 🙏
                                                                    Please consider donating to our open collective
                                                                           to help us maintain this package.


                                                                  🍷  Donate: https://opencollective.com/nest

npm run startすればよいみたいですね。

ちなみに、察話圢匏ではなくコマンドラむンオプションで指定したければ、以䞋のような感じになりたす。

$ npx nest new --language TypeScript --package-manager npm [アプリケヌション名]

--languageは付䞎しなくおもデフォルトでTypeScriptなのですが、なんずなく 。

nest new埌のディレクトリは、こんな感じです。

$ ll
合蚈 648
drwxrwxr-x   6 xxxxx xxxxx   4096  3月 22 23:59 ./
drwxrwxr-x   3 xxxxx xxxxx   4096  3月 22 23:54 ../
-rw-rw-r--   1 xxxxx xxxxx    631  3月 22 23:55 .eslintrc.js
drwxrwxr-x   7 xxxxx xxxxx   4096  3月 22 23:56 .git/
-rw-rw-r--   1 xxxxx xxxxx    391  3月 22 23:56 .gitignore
-rw-rw-r--   1 xxxxx xxxxx     51  3月 22 23:55 .prettierrc
-rw-rw-r--   1 xxxxx xxxxx   3339  3月 22 23:55 README.md
-rw-rw-r--   1 xxxxx xxxxx     64  3月 22 23:55 nest-cli.json
drwxrwxr-x 487 xxxxx xxxxx  20480  3月 22 23:56 node_modules/
-rw-rw-r--   1 xxxxx xxxxx 584349  3月 22 23:56 package-lock.json
-rw-rw-r--   1 xxxxx xxxxx   2011  3月 22 23:55 package.json
drwxrwxr-x   2 xxxxx xxxxx   4096  3月 22 23:55 src/
drwxrwxr-x   2 xxxxx xxxxx   4096  3月 22 23:55 test/
-rw-rw-r--   1 xxxxx xxxxx     97  3月 22 23:55 tsconfig.build.json
-rw-rw-r--   1 xxxxx xxxxx    546  3月 22 23:55 tsconfig.json

゜ヌスコヌドおよびテストコヌドが配眮されおいるディレクトリ。

$ tree src test
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts
test
├── app.e2e-spec.ts
└── jest-e2e.json

0 directories, 7 files

蚭定ファむルも芋おみたしょう。

package.json

{
  "name": "nestjs-getting-started",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/platform-express": "^8.0.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^8.0.0",
    "@nestjs/schematics": "^8.0.0",
    "@nestjs/testing": "^8.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "27.4.1",
    "@types/node": "^16.0.0",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "^27.2.5",
    "prettier": "^2.3.2",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "^27.0.3",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "^3.10.1",
    "typescript": "^4.3.5"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

nest-cli.json

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src"
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}

tsconfig.build.json

{
  "extends": "./tsconfig.json",
  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

.prettierrc

{
  "singleQuote": true,
  "trailingComma": "all"
}

.eslintrc.js

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  ignorePatterns: ['.eslintrc.js'],
  rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
  },
};

゜ヌスコヌドも芋おみたしょう。

srcディレクトリ内のファむルの圹割は、以䞋に曞かれおいたす。

First steps / Setup

アプリケヌションの゚ントリヌポむント。

src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

アプリケヌションのルヌトモゞュヌル。

Modules | NestJS - A progressive Node.js framework

src/app.module.ts

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

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

サヌビス。

src/app.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

コントロヌラヌ。

Controllers | NestJS - A progressive Node.js framework

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()
  getHello(): string {
    return this.appService.getHello();
  }
}

コントロヌラヌのテストコヌド。

src/app.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

こちらはナニットテストですね。

Testing / Unit testing

testディレクトリにあるのは、E2Eテストのようです。

Testing / End-to-end testing

test/app.e2e-spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

E2Eテストでの、Jestの蚭定ファむル。

test/jest-e2e.json

{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  }
}

動かしおみる

ずりあえず、この状態で起動しおみたしょうか。

$ npm start

起動したした。

> nestjs-getting-started@0.0.1 start
> nest start

[Nest] 61845  - 2022/03/23 0:22:49     LOG [NestFactory] Starting Nest application...
[Nest] 61845  - 2022/03/23 0:22:49     LOG [InstanceLoader] AppModule dependencies initialized +104ms
[Nest] 61845  - 2022/03/23 0:22:49     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 61845  - 2022/03/23 0:22:49     LOG [RouterExplorer] Mapped {/, GET} route +3ms
[Nest] 61845  - 2022/03/23 0:22:49     LOG [NestApplication] Nest application successfully started +2ms

こちらに埓っお、アクセスしおみたす。

First steps / Running the application

$ curl localhost:3000
Hello World!

src/app.module.tsでは、3000ポヌトをリッスンするようになっおいたした。

ルヌティングは、ログに定矩が出力されおいたしたね。

[Nest] 61845  - 2022/03/23 0:22:49     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 61845  - 2022/03/23 0:22:49     LOG [RouterExplorer] Mapped {/, GET} route +3ms

コントヌロヌラヌのURLは、どのように芋たらいいのでしょうか

ルヌティングの説明を芋るず、@Controllerデコレヌタヌでパスを指定するようですね。

Controllers / Routing

今回は特になにも指定されおいなかったので、そのたた/ルヌトにマッピングされたした、ず。

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

@Getデコレヌタヌにもパスを指定できるようです。

コントロヌラヌを远加しおみる

ここに、コントロヌラヌを远加しおみたしょう。

$ npx nest g controller hello

gは、generateサブコマンドの短瞮圢です。

実行結果。2぀のファむルが䜜成され、src/app.module.tsが曎新されたようです。

CREATE src/hello/hello.controller.spec.ts (485 bytes)
CREATE src/hello/hello.controller.ts (99 bytes)
UPDATE src/app.module.ts (326 bytes)

生成されたファむルを芋おみたしょう。

sc/hello/hello.controller.ts

import { Controller } from '@nestjs/common';

@Controller('hello')
export class HelloController {}

src/hello/hello.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { HelloController } from './hello.controller';

describe('HelloController', () => {
  let controller: HelloController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [HelloController],
    }).compile();

    controller = module.get<HelloController>(HelloController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });
});

src/app.module.tsは、以䞋のように生成したHelloControllerが远加されおいたす。

src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloController } from './hello/hello.controller';

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

nest generateでは、このようにファむルの远加や倉曎もできるようです。どのような皮類のファむルを生成できるかは、以䞋のペヌゞに
䞀芧衚がありたす。

CLI command reference / nest generate

生成されたコントロヌラヌを、ちょっず修正しおみたしょう。

src/hello/hello.controller.ts

import { Body, Controller, Get, Post, Query } from '@nestjs/common';

@Controller('hello')
export class HelloController {
  @Get('get')
  async getMessage(@Query('message') message: string): Promise<string> {
    return `Hello ${message}!!`;
  }

  @Post('post')
  async postMessage(@Body() request: any): Promise<any> {
    return {
      responseMessage: `Hello ${request.message}!!`,
    };
  }
}

@Queryや@Bodyの䜿い方やasyncに぀いおは、以䞋あたりを参考に。

Controllers / Request object

Controllers / Asynchronicity

開発䞭は、start:devでアプリケヌションを起動しおおくず、゜ヌスコヌドの修正に合わせおリロヌドしおくれそうです。

$ npm run start:dev

動䜜確認。

$ curl localhost:3000/hello/get?message=NestJS
Hello NestJS!!


$ curl -XPOST -H 'Content-Type: application/json' localhost:3000/hello/post -d '{"message": "NestJS" }'
{"responseMessage":"Hello NestJS!!"}

OKですね。

テストコヌドも修正しおおきたしょう。

src/hello/hello.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { HelloController } from './hello.controller';

describe('HelloController', () => {
  let controller: HelloController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [HelloController],
    }).compile();

    controller = module.get<HelloController>(HelloController);
  });

  it('should return "Hello NestJS!!"', async () => {
    expect(await controller.getMessage('NestJS')).toBe('Hello NestJS!!');
  });

  it('should return "Hello NestJS!!" object', async () => {
    expect(await controller.postMessage({ message: 'NestJS' })).toEqual({
      responseMessage: 'Hello NestJS!!',
    });
  });
});

今回はここたでにしおおきたしょうか。

たずめ

NestJSを初めお䜿っおみたした。

CLIでセットアップできお簡単に始められるのず、ドキュメントに沿っお進めおいくず基本的なこずはすぐにできそうな気がしたすね。

Node.jsでサヌバヌサむドアプリケヌションを曞く時には、ExpressよりもNestJSかな、ずちょっず思い始めたした。