nestjsとSqliteで簡単なAPIを作る


NestJSで使用できるORMであるTypeORMから、SQLiteを利用してデータベースを作り、APIを公開してみる。参考にしたのは以下のリンク。

エンティティを作る

ここでは、id, name, descriptionというメンバを持つ簡単なエンティティItemを作成する。idは自動生成にするため、@PrimaryGeneratedColumnデコレータを付ける。

item.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Item {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  name: string;

  @Column('text')
  description: string;
}

サービスを作る

アプリケーションが実行されている間、データベースにアクセスするために、サービスを作る。サービスはAngularと同様@Injectableデコレータを付けて定義する。

コンストラクタで引数にItemエンティティのリポジトリを与えている。この辺は定型のようなのでサンプルに倣って書く。

item.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Item } from './entities/item.entity';

@Injectable()
export class ItemService {
    constructor( @InjectRepository( Item ) private readonly itemRepos: Repository<Item> ) {
    }

    async add( item: Item ): Promise<Item> {
        let newItem = this.itemRepos.create( item );
        return await this.itemRepos.save(newItem);
    }

    async findAll(): Promise<Item[]> {
        return await this.itemRepos.find();
    }
}

addで使用しているcreateメソッドは新しいエンティティを作るが、それだけではデータベースに保存されない。saveすることでデータベースに保存されるので注意する。

コントローラを作る

作成したItemServiceを外部から利用できるよう、リクエストを受け取ってサービスとやりとりをするコントローラを作る。

まず、コンストラクタではItemServiceをInjectする。(Angularと同じ)

HTTPリクエストへの応答は、メソッドに@Get, @Postデコレータで対象のHTTPメソッドとルート(Route)指定することで定義する。戻り値はそのまま利用者に返されるオブジェクトになる。

メソッドには引数としてHTTPリクエストに含まれる情報を指定することができる。@Body(param)を指定すれば、リクエストボディに含まれるparamの値が引数で渡されるようになる。以下の例では、リクエストボディからnamedescriptionの値を取得し、新しいオブジェクトを作るのに使用している。

以下の例ではIDを自動生成するよう設定しているので、idの値にはundefinedを指定した。

item.controller.ts
import { Get, Post, Body, Controller } from '@nestjs/common';

@Controller()
export class ItemController {
    constructor(private readonly itemService: ItemService) {}

    @Get('/item')
    findAll():  Promise<Item[]> {
        return this.itemService.findAll()
    }

    @Post('/item')
    add(
        @Body('name') name: string,
        @Body('description') description: string
    ): Promise<Item> {
        return this.itemService.add( {id: undefined, name: name, description: description} );
    }
}

追記 @Controller('ルート')にすれば、わざわざメソッドごとにルートを書かなくてよかった。

モジュールを作る

これは必須ではないが、機能ごとにモジュールを分けておくと良いらしい。サービスをproviders、コントローラをcontrollers、そしてエンティティをimports内のforFeature( [ エンティティ ] )に追加する。

item.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Item])],
  providers: [ItemService],
  controllers: [ItemController],
})
export class ItemModule {}

アプリケーションに組み込む

importsの中に、使用するモジュールを列挙する。

データベースの本体として、TypeOrmModule.forRoot(オプション)をインポートすれば、entitiesで指定した対象のエンティティに対して、そのデータベースが使用できるようになる。複数データベースの本体をインポートすることができるので、マスタデータやトランザクションデータでデータベースを使い分けることもできるらしい。

entititesに文字列を追加すると、パターンマッチするファイルが公開するエンティティに対応するようになる。直接Itemとしても良いので、楽な書き方を選べばよい。

entitiesの指定を間違うと、以下の様なエラーが出てしまうので、その時は正しく指定できたか確認すること。

No repository for "Entity名" was found. Looks like this entity is not registered in current "default" connection

app.module.ts
import { ItemModule } from './sample.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
    imports: [ 
        TypeOrmModule.forRoot( {
            type: 'sqlite',
            database: 'db/sqlitedb.db',
            synchronize: true,
            entities: [ 'src/entities/*.entity.ts' ]
        } ), ItemModule
    ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

実行する

npm startで実行後、何度か適当に'localhost:3000/item'にPOSTし、その後GETした結果、以下のようにPOSTしたItemが列挙された。

まとめ

以下を用意すれば、APIぽいものを作ることができることが分かった。LoopBack4よりシンプルで楽に書くことができてしまった。LoopBackのあのごちゃごちゃしたオマジナイデコレータは何だったのか…

  • Itemエンティティを作る
  • ItemサービスでItemエンティティを保存するリポジトリの操作を提供する
  • ItemコントローラでItemサービスの提供する操作をAPIとして公開する
  • Itemモジュールで上記3つをまとめる(optional)
  • アプリケーションでItemエンティティを保持するデータベースとItemモジュールをインポートする

注意すること

デコレータが色々やってくれる分、見えない制約につまづく可能性があることが分かった。
サービスとしてリポジトリを作った際に、ItemServiceではなくItemRepositoryとしたところ、エラーもなくアプリケーションが起動しなくなってしまった。@InjectableRepository<Entity名>にすると、勝手に〇〇Repositoryというクラスが作られ、うまくいかなくなるのかもしれない。特に目立ったエラーなく起動しない場合は、命名を見直す必要がありそう。

[nodemon] restarting due to changes...
[nodemon] starting `ts-node -r tsconfig-paths/register src/main.ts`
[Nest] 1228   - 2018-12-2 00:28:36   [NestFactory] Starting Nest application...
[Nest] 1228   - 2018-12-2 00:28:36   [InstanceLoader] TypeOrmModule dependencies initialized +135ms
[Nest] 1228   - 2018-12-2 00:28:36   [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 1228   - 2018-12-2 00:28:36   [InstanceLoader] TypeOrmCoreModule dependencies initialized +38ms
[Nest] 1228   - 2018-12-2 00:28:36   [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[nodemon] clean exit - waiting for changes before restart
本来ならこの後で以下が続くはずなのに、勝手に止まってしまう。
[RouterResolver]
[RouterExplorer]