これは、なにをしたくて書いたもの?
Node.jsのサーバーサイドフレームワークといえば、ExpressやFastifyあたりが有名なのかなと思っていたのですが。
- Express - Node.js web application framework
- Fastify, Fast and low overhead web framework, for Node.js
「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
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のサポート
- JavaScriptでも開発可能
- 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に沿って進めてみましょう。
First steps | NestJS - A progressive Node.js framework
環境
今回の環境は、こちら。
$ node --version v16.14.2 $ npm --version 8.5.0
NestJSプロジェクトを作成する
NestJSプロジェクトの作成方法は、Nest CLIを使う方法とstarterプロジェクトをクローンするかのどちらかになるようです。
※結果は同じだそうです
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
でプロジェクトを作成するようですが、どちらを使用するかでプロジェクトの構造が変わるようです。
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
ディレクトリ内のファイルの役割は、以下に書かれています。
アプリケーションのエントリーポイント。
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!'); }); }); });
こちらはユニットテストですね。
test
ディレクトリにあるのは、E2Eテストのようです。
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
デコレーターでパスを指定するようですね。
今回は特になにも指定されていなかったので、そのまま/
(ルート)にマッピングされました、と。
@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
については、以下あたりを参考に。
開発中は、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かな、とちょっと思い始めました。