LoginSignup
3
0

More than 3 years have passed since last update.

NestJS で TypeORM を使った API を作成する

Last updated at Posted at 2020-05-31

コードサンプル

ここで作成したコード全文は Github から確認できる(若干違うかも)
https://github.com/tktcorporation/nestjs-sample/tree/create-some-api-with-typeorm

DB の準備

Dockerfile

FROM node:12.16.1

RUN npm i -g @nestjs/cli ts-node

ENTRYPOINT ["/bin/bash", "-c"]

docker-compose

version: "3"
services:
  app:
    build: .
    container_name: app
    working_dir: /app
    volumes:
      - {プロジェクトディレクトリ}:/app
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_USER=root
      - DB_PASSWORD=password
      - DB_DATABASE_NAME=database
      - NODE_ENV=development
    depends_on:
      - db
  db:
    image: postgres:11
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: database
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
    hostname: postgres
    restart: always
    user: root

別段の記述がない限り、以降の操作はすべて Docker コンテナ内にて行う

必要なモジュールの追加

$ yarn add @nestjs/typeorm typeorm pg

(@nestjs/typeorm はいらないかも)

接続に必要な設定を書いていく

  • nestjs 標準でついている連携機構(?)は使わない
    • 標準でついている連携機構(?) -> TypeOrmModule.forRoot() をインポートする方法
  • コネクション管理用のクラスを別で作成して使用する
  • cli は ormconf.ts の設定を適用して実行する(ormconfig だとアプリ側で自動適用されたりしてよくわからなくなった)

もっと良い方法があればご教示お願いしたく…
(ちょちょいと書いたらおわりでしょーとか思ってたら見事に嵌ってしばらく抜け出せなかった)

dbconnection.ts

  • コネクション管理用のクラスを別で作成して使用する

に当たる部分。

こんな感じ(TypeORM をシングルトンで使う) のファイルを作成する

ormconf.js

module.exports = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: Number.parseInt(process.env.DB_PORT),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE_NAME,
  synchronize: false,
  logging: process.env.NODE_ENV === 'production' ? ['error'] : 'all',
  migrations: ['src/migration/**/*.ts'],
  cli: {
    migrationsDir: 'src/migration',
  },
};

マイグレーション

この記事では、こちら で書かれているような Repository パターンではなく、SQL を直接書く方法で DB を扱っていきます。
複雑な SQL を書く場合、O/R Mapper だとうまく書けないことが多いので、最近はこの方法で書くことが多いです。
ただ、実際の Table の型と SELECT してきたときの型が一致しないので、ちゃんとテストを書いておかないと Table の構成を変更したときのエラーを拾いきれなくなるかもです。

ファイル作成コマンド

$ ts-node $(npm bin)/typeorm migration:create -f ormconf -n {ファイル名}

このコマンドで00000000000000-{ファイル名}.ts のファイルが作成される。

SQL を書いていく

import { MigrationInterface, QueryRunner } from 'typeorm';

export class {ファイル名}00000000000000 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        // この中に実行するSQLを書く
        await queryRunner.query(`
            CREATE TABLE "users" (
                "id" SERIAL NOT NULL,
                "name" VARCHAR(250) NOT NULL
            );
        `);
    }

    // この中には実行したSQLの動作を一つ戻すためのものを書く
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`
            DROP TABLE "users";
        `);
    }
}

マイグレーション実行

このコマンドで、作成したマイグレーションファイルを実行できる。

ts-node $(npm bin)/typeorm migration:run

こんな感じに表示されれば成功

migration 実行ログ
$ ts-node $(npm bin)/typeorm migration:run
query: SELECT * FROM "information_schema"."tables" WHERE "table_schema" = current_schema() AND "table_name" = 'migrations'
query: SELECT * FROM "migrations" "migrations" ORDER BY "id" DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations that needs to be executed.
query: START TRANSACTION
query: 
            CREATE TABLE "users" (
                "id" SERIAL NOT NULL,
                "name" VARCHAR(250) NOT NULL
            );

query: INSERT INTO "migrations"("timestamp", "name") VALUES ($1, $2) -- PARAMETERS: [1590802150528,"CreateUsersTable1590802150528"]
Migration CreateUsersTable1590802150528 has been executed successfully.
query: COMMIT
Done in 9.74s.

DBデータの操作

せっかくなので、API が呼ばれた時に INSERT, SELECT が走るようにする。

テストの実行

$ yarn test

補足

これ以降の実装をした後にテストを実行すると、どこかで下記のようなエラーが発生するかもしれない。

 FAIL  src/infrastructure/dbconnection.spec.ts
  ● Test suite failed to run

    Cannot find module 'src/infrastructure/dbconnection' from 'dbconnection.spec.ts'

      1 | import { Connection } from 'typeorm';
    > 2 | import { DBConnection } from 'src/infrastructure/dbconnection';
        | ^
      3 | 
      4 | describe('DBConnection', () => {
      5 |   let connection: Connection;

      at Resolver.resolveModule (../node_modules/jest-resolve/build/index.js:299:11)
      at Object.<anonymous> (infrastructure/dbconnection.spec.ts:2:1)

その場合は、 次の操作を行う

  • package.json の編集
{
  "name": "hoge", 
  ...
  "jest": {
    "rootDir": ".",
    "moduleNameMapper": {
      "src(.*)$": "<rootDir>/src/$1"
    }
  }
}
  • test/jest-e2e.json の編集
{
  "moduleFileExtensions": ["js", "json", "ts"],
  ...
  "rootDir": ".",
  "moduleNameMapper": {
    "src(.*)$": "<rootDir>/../src/$1"
  },
}

これでテストが問題なく走るようになったはず。

DAO の作成

DB からデータを取ってくる部分は DAO(DataAccessObject) で

src/dao/users.dao.ts

import { EntityManager } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';

export class UsersDao {
  constructor(private readonly manager: EntityManager) {}
  create = (name: string): Promise<unknown> =>
    this.manager.query(`INSERT INTO "users" ("name") VALUES ($1);`, [name]);
  findAll = async (): Promise<UsersDao.Entity[]> => {
    const users: unknown[] = await this.manager.query(`
            SELECT
                id,
                name
            FROM
                "users" AS users;
        `);
    return users.map(user => UsersDao.buildEntity(user));
  };
  findOneById = async (id: string): Promise<UsersDao.Entity | undefined> => {
    const user: unknown[] = await this.manager.query(
      `
            SELECT
                users. id,
                users. name
            FROM
                "users"
            WHERE
                users. "id" = $1;
        `,
      [id],
    );
    if (user.length < 1) return undefined;
    return UsersDao.buildEntity(user[0]);
  };

  private static buildEntity(object: unknown): UsersDao.Entity {
    if (!UsersDao.isEntity(object))
      throw new InternalServerErrorException(
        object,
        'fetched data type is invalid',
      );
    return {
      id: object.id,
      name: object.name,
    };
  }
}
export namespace UsersDao {
  export interface Entity {
    id: number;
    name: string;
  }
  export const isEntity = (value: unknown): value is Entity => {
    return (
      typeof value === 'object' &&
      typeof (value as Entity).id === 'number' &&
      typeof (value as Entity).name === 'string'
    );
  };
}

テスト

users.dao.spec.ts
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from './users.dao';

describe('UsersDao', () => {
  let dao: UsersDao;

  beforeEach(async () => {
    dao = new UsersDao(await DBConnection.getManager());
  });

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

  it('create', async () => {
    const result = await dao.create('JohnDoe');
  });
  it('get', async () => {
    const users = await dao.findAll();
    expect(users).toBeDefined();
    if (users.length < 1) return;
    expect(users[0].id).toBeGreaterThan(0);
    expect(users[0].name.length).toBeGreaterThan(0);
  });
  it('getById', async () => {
    const id = '1';
    const device = await dao.findOneById(id);
    if (device) {
      expect(device.id).toBeGreaterThan(0);
      expect(device.name.length).toBeGreaterThan(0);
    }
  });
  afterEach(async () => {
    await DBConnection.close();
  });
});

サービスの実装

動作のひとかたまりをサービスに置いて、コントローラから呼び出す

src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from 'src/dao/users.dao';

@Injectable()
export class UsersService {
  createFullName = (params: {
    firstName?: string;
    lastName?: string;
  }): string => `${params.firstName}${params.lastName}`;

  createUser = async (params: {
    firstName?: string;
    lastName?: string;
  }): Promise<unknown> =>
    (await DBConnection.getManager())
      .transaction(manager =>
        new UsersDao(manager).create(
          this.createFullName({
            firstName: params.firstName,
            lastName: params.lastName,
          }),
        ),
      )
      .finally(() => DBConnection.close());
  getAllUser = async (): Promise<UsersDao.Entity[]> =>
    (await DBConnection.getManager())
      .transaction(manager => new UsersDao(manager).findAll())
      .finally(() => DBConnection.close());
}

テスト

users.service.spec.ts
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from './users.dao';

describe('UsersDao', () => {
  let dao: UsersDao;

  beforeEach(async () => {
    dao = new UsersDao(await DBConnection.getManager());
  });

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

  it('create', async () => {
    const result = await dao.create('JohnDoe');
    expect(result).toStrictEqual([]);
  });
  it('get', async () => {
    const users = await dao.findAll();
    expect(users).toBeDefined();
    if (users.length < 1) return;
    expect(users[0].id).toBeGreaterThan(0);
    expect(users[0].name.length).toBeGreaterThan(0);
  });
  it('getById', async () => {
    const id = '1';
    const device = await dao.findOneById(id);
    if (device) {
      expect(device.id).toBeGreaterThan(0);
      expect(device.name.length).toBeGreaterThan(0);
    }
  });
  afterEach(async () => {
    await DBConnection.close();
  });
});

コントローラの作成

API の呼び出しはここが起点になる

src/users/users.controller.ts

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

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post()
  async createUser(
    @Body() createFullNameDto: { first_name?: string; last_name?: string },
  ) {
    await this.usersService.createUser({
      firstName: createFullNameDto.first_name,
      lastName: createFullNameDto.last_name,
    });
    return {
      method: 'post',
      message: 'created',
    };
  }

  @Get()
  async getAllUsers() {
    return {
      users: await this.usersService.getAllUser(),
    };
  }
}

テスト

users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('Users Controller', () => {
  let controller: UsersController;

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

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

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

  it('create user', async () => {
    const first = 'John';
    const last = 'Doe';
    const result = await controller.createUser({
      first_name: first,
      last_name: last,
    });
    expect(result.message).toBe(`created`);
  });

  it('get all users', async () => {
    const result = await controller.getAllUsers();
    expect(result.users.length).toBeGreaterThan(0);
    expect(result.users[0].id).toBeGreaterThan(0);
    expect(result.users[0].name.length).toBeGreaterThan(0);
  });
});

統合テストの追加

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';
import { UsersDao } from 'src/dao/users.dao';

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

  beforeAll(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!');
  });

  it('/users (POST)', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ first_name: 'John', last_name: 'Doe' })
      .expect(201)
      .expect(JSON.stringify({ method: 'post', message: 'created' }));
  });

  it('/users (GET)', async () => {
    const response = await request(app.getHttpServer())
      .get('/users?first_name=John&last_name=Doe')
      .expect(200);
    const body = response.body as {
      users: UsersDao.Entity[];
    };
    expect(body.users).toBeDefined();
    if (body.users.length < 1) return;
    expect(body.users[0].id).toBeGreaterThan(0);
    expect(body.users[0].name.length).toBeGreaterThan(0);
  });
});

API にアクセスしてみる

  • NestJS サーバーの起動
$ yarn start
  • Docker コンテナ外からアクセス
$ curl -X GET "http://{DockerIP}:3000/users" -H "accept: */*"
{"users":[{"id":1,"name":"JohnDoe"}]}

この手順 (NestJS でプロジェクト作成から API ドキュメントの表示まで ) でAPIドキュメント表示用の設定をしておけば、 http://{DockerIP}:3000/api にアクセスすることで、自由にAPIを試すことができる。

Screen Shot 2020-05-31 at 12.16.29.png

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0