🐅

気になる人向けの NestJS入門

2023/12/22に公開

この記事はNestJS気になるけど
自分はexpressで良いかなぁ・・・、honoで良いかなぁ・・・となっている人向けの記事です。
API開発完全初心者の方にはあまり向いていません。

自分もHonoOakを使っていますが、NestJSAngularに準拠した書き方が気になったので入門してみます。

セットアップ

NestJS公式が出しているCLIツールでセットアップをしてみます。

npm install -g @nestjs/cli
pnpm install -g @nestjs/cli
nest new sample-app

これでアプリを新規作成可能です。
この時点でもうAngularに似てますね。

コードを見て見る

ファイル構成はこのような感じです。

// 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 {}

滅茶苦茶Angularです。

ちなみにStackBlitzを用いてテストしています。
使い方はこちら

そしてここが一番重要です。

// 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();
  }
}

ここではGETでリクエストが送信された時、

// app.service.ts

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

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

このクラスにある getHello メソッドを実行するようにしています。

起動してみる

// 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();

main.tsをエントリーポイントにして起動する用です。

app.listen([[port]])見慣れた奴ですね

起動して、localhost:3000を開きます。

ちゃんと表示できてます。

新しいエンドポイントを作る

サーバー側の時間を返すエンドポイントを作ってみます。

// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
  getTime(): string {
    return new Date(Date.now()).toLocaleDateString();
  }
}

今の時間を返す getTime メソッドを作成しました。
これをルーティングしてみます。

// 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();
  }

  @Get("time")
  getTime(): string {
    return this.appService.getTime()
  }
}

https://ame-x.net

しっかりルーティング出来てます。

HonoExpressでも出来る機能を紹介します。(全てでは有りません)

その他

POSTメソッドや他のメソッドの利用

何となく察しているかも知れませんが、

// app.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("time")
  getTime(): string {
    return this.appService.getTime()
  }

  @Post("time-post")
  getTimePost(): string {
    return this.appService.getTime()
  }
}

@Postデコレーターでルーティング可能です。

しっかり取得できてます。
また他のmethodも利用可能です。

動的ルーティング

// app.controller.ts
import { Controller, Get, Post, Param } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("time")
  getTime(): string {
    return this.appService.getTime()
  }

  @Post("time-post")
  getTimePost(): string {
    return this.appService.getTime()
  }

  @Post('name/:param')
  getPathParameters(@Param('param') param: string): string {
    return `My Name is ${param}`;
  }
}

この様に癖が無く、快適です。

basePath

クラスの @Controller デコレーターの中に文字列を入れればOKです。

これにより /api を基準としてアクセスできます。
物凄く簡単

ワイルドカード

これも動的ルーティングとほぼ同じです。

// app.controller.ts
import { Controller, Get, Post, Param } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("time")
  getTime(): string {
    return this.appService.getTime()
  }

  @Post("time-post")
  getTimePost(): string {
    return this.appService.getTime()
  }

  @Post('name/:param')
  getPathParameters(@Param('param') param: string): string {
    return `My Name is ${param}`;
  }

  @Post('i-am-*')
  getGreet(): string {
    return "hi!"
  }
}

リクエストのオブジェクト (コンテキスト)

// app.controller.ts
import { Controller, Get, Post, Param, Req } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("time")
  getTime(): string {
    return this.appService.getTime()
  }

  @Post("time-post")
  getTimePost(): string {
    return this.appService.getTime()
  }

  @Post('name/:param')
  getPathParameters(@Param('param') param: string): string {
    return `My Name is ${param}`;
  }

  @Post('i-am-*')
  getGreet(): string {
    return "hi!"
  }

  @Post('ctx')
  getCtx(@Req() req): string {
    console.log(req)
    return "hello!"
  }
}

ただしリクエストのコンテキストを取得したい場合、普通は@Bodyデコレーター等を使うのがエレガントです。

ルーティングファイル分割

ここでそろそろファイルが肥大化してきたので新しくコントローラーを作ります。
app.controller.ts のようなファイルを新しく作り、app.module.tsに登録するだけです。

// app2.controller.ts
import { Controller, Get, Post, Param, Req } from '@nestjs/common';
import { AppService } from './app.service';

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppController as App2Controller } from './app2.controller';
import { AppService } from './app.service';

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

これで完成です。
超簡単

Query

// app2.controller.ts
import { Controller, Get, Post, Query } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("query")
  getQuery(@Query("id") id): string {
    return id;
  }
}

@Queryの引数がパラメーターの名前です。

Body

// app2.controller.ts
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { AppService } from './app.service';

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

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

  @Get("query")
  getQuery(@Query("id") id): string {
    return id;
  }

  @Post("body")
  getBody(@Body("id") id): string {
    console.log(id);
    return "hey body!";
  }
}

レスポンスヘッダー

// app2.controller.ts
import { Body, Controller, Get, Post, Query, HttpCode, Header } from '@nestjs/common';
import { AppService } from './app.service';

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

  @Get()
  @HttpCode(201)
  @Header("Content-Type", "application/json")
  getHello(): string {
    return JSON.stringify({
        "name": "amex2189"
    });
  }

  @Get("query")
  getQuery(@Query("id") id): string {
    return id;
  }

  @Post("body")
  getBody(@Body("id") id): string {
    console.log(id)
    return "hey body!"
  }
}

エラーハンドリング

new HttpException('Forbidden', HttpStatus.FORBIDDEN)

等を返すことで可能です。

その他実装のヒント

パラメーター不足等のハンドリング

パラメーターが足りない場合等のハンドリングは PIPE と呼ばれる機能を使うと良いです。 (Angularにもある通りtransform関数の引数にクエリを入れて実行し、好きな形で返せます。)

ログイン機能

GUARD と呼ばれる機能でログインしてない場合、区別しハンドリングという事が可能です。
DATABASE
AUTH

関連記事

最後に

基本的な物は以上です。
個人的にHonoExpressとは違う堅牢性やAngularのようなファイル分割でロジックの分割が上手くできていてこれなら保守も楽そうだなと感じました。
Honoにも高性能なRoute機能が有りますが、これとはまた違った使用感です。
更にWebSocket対応や、ORMの対応がHonoよりも優れていると感じました。
最初の開発体験はHonoと比べると劣ってしまいますが、それも慣れていくにつれて改善されるものだと思います。
今回作ったAPIのソースはここに有ります。

ちなみに開発モードは

npm run start:dev
pnpm run start:dev

で可能です。

最後まで読んでいただきありがとうございます。
良ければハートを教えて頂けるとありがたいです。

関連リンク

公式ドキュメント
今回使用したリポジトリ
Hono
Express

Discussion