備忘69

個人的な備忘録です。細かいtips多め。


nodejs用フレームワークnestjsのサンプルを触ってみた(1)

公式docs

https://docs.nestjs.com/
早い話がサーバーサイド版Angular(?)

cats-app example

https://github.com/nestjs/nest/tree/master/examples/01-cats-app

起動

git clone https://github.com/nestjs/nest.git  
cd ./nest/examples/01-cats-app/
npm i  
npm run start

localhost:3000 にサーバーが立ち上がります。

とりあえず基本的な部分を使ってみる

その前に cats.controller.ts の中身を一部変更する。

  @Post()
  // @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
  }

権限の問題を避けるため @Roles デコレーターをコメントアウトした。 ユーザー権限をメソッド単位で管理できるのは便利そう。

create() メソッドを動かすためのPOSTリクエストを投げてみる。

##Doraemon を追加
$curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "name":"Doraemon","age":12,"breed":"R-01FR001-MKII"}' http://localhost:3000/cats/
##Doramichan を追加
$curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "name":"Doramichan","age":10,"breed":"R-01FR001-MKII"}' http://localhost:3000/cats/

次に findAll() メソッドを動かすためのGETリクエストを投げてみる。

$curl http://localhost:3000/cats/
##catsが帰ってくる。
{"data":[{"name":"Doraemon","age":12,"breed":"R-01FR001-MKII"},{"name":"Doramichan","age":10,"breed":"R-01FR001-MKII"}]}

こんな感じの簡単なHTTPサーバーになっていることがわかる。

構成

/nest/examples/01-cats-app/src$ tree
.
├── app.module.ts
├── cats
│   ├── cats.controller.spec.ts
│   ├── cats.controller.ts  ★
│   ├── cats.module.ts
│   ├── cats.service.ts  ★
│   ├── cats.service.spec.ts  ★(作成)

│   ├── dto
│   │   └── create-cat.dto.ts
│   └── interfaces
│   └── cat.interface.ts
(略)
└── main.ts

★...とりあえず今回見てみたところ

コントローラー

クライアントからリクエストを受けてレスポンスを返すクラス。API と処理を記述。
cats/cats.controller.ts

(略)
@Controller('cats')
@UseGuards(RolesGuard)
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  // @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
  return this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
  return await this.catsService.findAll();
  }

  @Get(':name')
  async findOne(@Param() params):Promise<Cat> {
  return await this.catsService.findOne(params.name);
  }
}

constructor で catsServiceを消費しているあたり Angular でいう *component.ts でDIしている感じ。

(参考:https://angular.io/guide/dependency-injection)

最後にGet http://localhost:3000/cats/:name を追加して、猫を名前で検索できるようにした。 @Param() は expressでいうところの req.params で、 params.name のようにプロパティにアクセスできる。

コンポーネント

コントローラー(や他のコンポーネント)にDIされるもの。サービスのコンポーネントにはDBとやりとりするためのSQLやORMを書く。
cat.service.ts

(略)
/* 追加 */
import { HttpException } from "@nestjs/core";

@Component()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
  this.cats.push(cat);
  return {"message":"success"};
  }

  findAll(): Cat[] {
  return this.cats;
  }

  findOne(name: String): Cat {
  let cat: Cat = this.cats.find(cat => cat.name == name);

  if (!cat) {
  throw new HttpException('Specified cat does not exist !', HttpStatus.BAD_REQUEST);
  } else {
  return cat;
  }

  }
}

テスト

cats.service に対するunitテストを書いた。
cat.service.spec.ts

import { Test } from '@nestjs/testing';;
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';

describe('CatsService', () => {
  let catsService: CatsService;

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

  catsService = module.get<CatsService>(CatsService);
  });

  describe('create Test', () => {
  it('should return a success message', async () => {
  let cat: Cat = { "name": "Doraemon", "age": 12, "breed": "R-01FR001-MKII" };
  let result = { "message": "success" };

  expect(await catsService.create(cat)).toEqual(result);
  });

  });

  describe('findAll Test', () => {
  it('should return an array of cats', async () => {
  let cat1, cat2: Cat;

  cat1 = { "name": "Doraemon", "age": 12, "breed": "R-01FR001-MKII" };
  cat2 = { "name": "Doramichan", "age": 10, "breed": "R-01FR001-MKII" };
  let result = [cat1,cat2];

  await catsService.create(cat1);
  await catsService.create(cat2);

  expect(await catsService.findAll()).toEqual(result);
  });
  });

  describe('findOne Test', () => {

  it('should retrive a specified cat from cats array', async () => {
  let cat1, cat2: Cat;

  cat1 = { "name": "Doraemon", "age": 12, "breed": "R-01FR001-MKII" };
  cat2 = { "name": "Doramichan", "age": 10, "breed": "R-01FR001-MKII" };

  await catsService.create(cat1);
  await catsService.create(cat2);

  expect(await catsService.findOne("Doraemon")).toEqual(cat1);
  expect(await catsService.findOne("Doramichan")).toEqual(cat2);
  });

  it('should return an error object when incoming name does not exist in cats array', async () => {
  let cat1, cat2: Cat;

  cat1 = { "name": "Doraemon", "age": 12, "breed": "R-01FR001-MKII" };
  cat2 = { "name": "Doramichan", "age": 10, "breed": "R-01FR001-MKII" };

  await catsService.create(cat1);
  await catsService.create(cat2);

  expect( () => { catsService.findOne("mi-chan") }).toThrow();

  });

  });

});

expect(FUNCTION).toThrow(); expectの引数はfunctionなので、無名関数の形で渡す必要がある。
(参考:https://ajsblackbelt.wordpress.com/2014/05/18/jasmine-tests-expect-tothrow/)

最後に作った猫検索を試してみる。

## Doraemon 取得
 $curl http://localhost:3000/cats/Doraemon/
{"data":{"name":"Doraemon","age":12,"breed":"R-01FR001-MKII"}}

##  Doramichan 取得
$ curl http://localhost:3000curl http://localhost:3000/cats/Doramichan/
{"data":{"name":"Doramichan","age":10,"breed":"R-01FR001-MKII"}}

##  mi-chan 取得
$ curl http://localhost:3000/cats/mi-chan/
{"statusCode":400,"message":"Specified cat does not exist !"}

感想

nodejs + express から typescript に移行するのが中々難しいなと思っていたので、nestjsのようなフレームワークの存在は非常にありがたいです。
様々な機能があるみたいなので、今後も使ってみようと思います。