🚀

TypeScript +NestJSをプロジェクトで導入したら素晴らしかった件

2021/12/18に公開約10,300字

概要

今回はTypescript製のバックエンドフレームワーク「NestJS」について紹介させていただきます

私が導入した時にお世話になった記事🙇‍♂️

https://qiita.com/elipmoc101/items/9b1e6b3efa62f3c2a166

記事のゴール

1.NestJSの概要を説明
2.NestJSを使用したREST APIをハンズオンで構築する
3.構築したAPIにリクエストを投げる

ここまでをゴールとします

NestJSとは?

https://nestjs.com/
https://github.com/nestjs/nest
  • 効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのフレームワーク
  • モダンなJavaScriptを使用し、TypeScript(純粋なJavaScriptとの互換性を保つ)で構築されている
  • OOP(オブジェクト指向プログラミング)、FP(関数型プログラミング)、FRP(関数型リアクティブプログラミング)の要素を兼ね備えている

let's NestJS!!(・∀・)

環境構築

https://docs.nestjs.com/first-steps#setup
# NestJSをセットアップするディレクトリを作成します
mkdir nestjs-sample 

# cliをインストールします
npm i -g @nestjs/cli

# プロジェクトを作成します
nest new sample 

# 今回はnpmでインストールします
? Which package manager would you ❤️  to use? npm

npm-scriptでnestを起動します

nestjs-sample/sample
# モジュールをインストールします
npm i 

# コードの変更を検知するwatchモードで起動
nest start --watch
package.json
  "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",
  },

自動的にCRUDを構築するベースを作成

  • NestJSに慣れるまではCRUDジェネレーターで作られた雛形をカスタマイズするといいかもです!

https://docs.nestjs.com/recipes/crud-generator#generating-a-new-resource
nestjs-sample/sample
nest g resource users
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/users/users.controller.spec.ts (566 bytes)
CREATE src/users/users.controller.ts (894 bytes)
CREATE src/users/users.module.ts (247 bytes)
CREATE src/users/users.service.spec.ts (453 bytes)
CREATE src/users/users.service.ts (609 bytes)
CREATE src/users/dto/create-user.dto.ts (30 bytes)
CREATE src/users/dto/update-user.dto.ts (169 bytes)
CREATE src/users/entities/user.entity.ts (21 bytes)
UPDATE src/app.module.ts (411 bytes)
✔ Packages installed successfully.

TypeORMとSQLiteのセットアップ

データベース

nestjs-sample/sample
npm i --save @nestjs/typeorm typeorm sqlite3

TypeORMの設定ファイルを用意します

nestjs-sample/sample
touch ormconfig.json
sample/ormconfig.json
{
    "type": "sqlite",
    "database": "data/dev.sqlite",
    "entities": [
        "dist/**/entities/**/*.entity.js"
    ],
    "migrations": [
        "dist/**/migrations/**/*.js"
    ],
    "logging": true
}

NestJSにTypeORMをセットアップ

sample/src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  //追加!
  imports: [AppModule, TypeOrmModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Entity定義

  • Entityとはテーブルの構造をクラス構文で表現したものです
sample/src/users/entities/user.entity.ts
import { Column, PrimaryGeneratedColumn, Entity } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn({
    comment: 'アカウントID',
  })
  readonly id: number;

  @Column('varchar', { comment: 'アカウント名' })
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

マイグレーションファイルの生成

nestjs-sample/sample
# コンパイルします
npm run build

# マイグレーションファイルを生成します
npx typeorm migration:generate -d src/database/migrations -n create-user
sample/src/database/migrations/1639847070691-create-user.ts
import {MigrationInterface, QueryRunner} from "typeorm";

export class createUser1639847070691 implements MigrationInterface {
    name = 'createUser1639847070691'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL)`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "users"`);
    }

}

マイグレーションを実行

nestjs-sample/sample
npm run build
npx typeorm migration:run

モジュールにTypeORMを組み込む

sample/src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

登録したリポジトリをサービスで依存解決します

sample/src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}
}

バリデーションを定義します

https://github.com/typestack/class-validator
nestjs-sample/sample
npm install class-validator --save
sample/src/users/dto/create-user.dto.ts
import { MaxLength } from 'class-validator';

export class CreateUserDto {
  @MaxLength(255, {
    message: 'アカウント名は255文字以内で入力してください',
  })
  name: string;
}

サービスクラスにロジックを定義します

  • 基本的なCRUDを作成します
sample/src/users/users.service.ts
import {
  HttpException,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}

  /**
   * 登録
   * @param createUserDto
   * @returns
   */
  async create(createUserDto: CreateUserDto): Promise<{ message: string }> {
    await this.userRepository
      .save({
        name: createUserDto.name,
      })
      .catch((e) => {
        throw new InternalServerErrorException(
          `[${e.message}]アカウントの登録に失敗しました。`,
        );
      });
    return { message: 'アカウントの登録に成功しました' };
  }

  /**
   * 一覧
   * @returns
   */
  async findAll(): Promise<User[]> {
    return await this.userRepository.find();
  }

  /**
   * 詳細
   * @param id
   * @returns
   */
  async findOne(id: number): Promise<User> {
    if (!id) throw new NotFoundException('IDが指定されていません');
    return await this.userRepository.findOne(id);
  }

  /**
   * 更新
   * @param id
   * @param updateUserDto
   * @returns
   */
  async update(
    id: number,
    updateUserDto: UpdateUserDto,
  ): Promise<{ message: string }> {
    if (!id) throw new NotFoundException('IDが指定されていません');

    await this.userRepository
      .update(id, {
        name: updateUserDto.name,
      })
      .catch((e) => {
        throw new InternalServerErrorException(
          `[${e.message}]アカウントID「${id}」の更新に失敗しました。`,
        );
      });
    return { message: `アカウントID「${id}」の更新に成功しました。` };
  }

  /**
   * 削除
   * @param id
   * @returns
   */
  async remove(id: number): Promise<{ message: string }> {
    if (!id) throw new NotFoundException('IDが指定されていません');

    await this.userRepository.delete(id).catch((e) => {
      throw new InternalServerErrorException(
        `[${e.message}]アカウントID「${id}」の削除に失敗しました。`,
      );
    });
    return { message: `アカウントID「${id}」の削除に成功しました。` };
  }
}

コントローラークラスでサービスクラスを呼び出します

sample/src/users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

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

  /**
   * 登録
   * @param createUserDto
   * @returns
   */
  @Post()
  async create(
    @Body() createUserDto: CreateUserDto,
  ): Promise<{ message: string }> {
    return await this.usersService.create(createUserDto);
  }

  /**
   * @summary 一覧
   * @returns
   */
  @Get()
  async findAll(): Promise<User[]> {
    return await this.usersService.findAll();
  }

  /**
   * @summary 詳細
   * @param id
   * @returns
   */
  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return await this.usersService.findOne(+id);
  }

  /**
   * @summary 更新
   * @param id
   * @param updateUserDto
   * @returns
   */
  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<{ message: string }> {
    return await this.usersService.update(+id, updateUserDto);
  }

  /**
   * @summary 削除
   * @param id
   * @returns
   */
  @Delete(':id')
  async remove(@Param('id') id: string): Promise<{ message: string }> {
    return await this.usersService.remove(+id);
  }
}

動作を確認します

nestjs-sample/sample
# 登録
curl -XPOST -H "Content-Type:application/json" localhost:3000/users -d '{"name":"サンプル太郎"}'   
{"message":"アカウントの登録に成功しました"}%

curl -XPOST -H "Content-Type:application/json" localhost:3000/users -d '{"name":"サンプル二郎"}'   
{"message":"アカウントの登録に成功しました"}%

# 一覧
curl -H "Content-Type:application/json" localhost:3000/users                           
[{"name":"サンプル太郎","id":1},{"name":"サンプル二郎","id":2}]%

# 詳細
curl -H "Content-Type:application/json" localhost:3000/users/1        
{"name":"サンプル太郎","id":1}%

# 更新
curl -XPATCH -H "Content-Type:application/json" localhost:3000/users/2 -d '{"name":"更新したよ"}'
{"message":"アカウントID「2」の更新に成功しました。"}% 

curl -H "Content-Type:application/json" localhost:3000/users                           
[{"name":"サンプル太郎","id":1},{"name":"更新したよ","id":2}]%

# 削除
curl -XDELETE -H "Content-Type:application/json" localhost:3000/users/2                   
{"message":"アカウントID「2」の削除に成功しました。"}%

curl -H "Content-Type:application/json" localhost:3000/users                           
[{"name":"サンプル太郎","id":1}]%

最後に

読んでいただきありがとうございます。
今回の記事はいかがでしたか?
・こういう記事が読みたい
・こういうところが良かった
・こうした方が良いのではないか
などなど、率直なご意見を募集しております。

Discussion

ログインするとコメントできます