こんにちは、次世代デジタル基盤開発事業部の野口です。 関わったプロジェクトで以下の構成でexpressでバックエンド開発をしたので、そのアウトプットとして記事を書きます。
- express
- routing-controllers
- inversify(DI)
- typeorm
ただ、この環境を作るのが難しそうなので、もっと簡単にCLI一発で似たような環境が作れるNestJSで作成していきたいと思います。本当はNestJSを触ってみたかっただけですが、、、
NestJSはNode.js上で動作するオープンソースのバックエンド開発フレームワークで expressをコアにして作られています。
詳しくは以下を読むと分かりやすいです。
NestJSにざっくり入門してみる
今回はtodoListのCRUD操作と簡単な検索機能を作成することをゴールとしたいと思います。
環境構築
- 開発環境
ubuntu-20.04
node v18.2.0
まずは、以下の手順で環境構築を行っていきます
- Nest CLIをインストール
npm i -g @nestjs/cli
- プロジェクトの作成
nest new todo-list
npmを選択します
以下のフォルダが生成されます。
以下のファイルは使用しないので削除してしまって大丈夫です。
app.controller.spec.ts
app.controller
app.service
todoListのモジュール、コントローラー、サービスを作ります。 nestではCLIで作成できるのでターミナルから実行します。
nest g module todos
nest g controller todos --no-spec
nest g service todos --no-spec
ちなみに、--no-spec
はテストファイルを作成しないという意味なのでテストファイルが欲しい場合はなしでOKです。
- typeormのインストール
npm install typeorm@0.2.45 @nestjs/typeorm pg -f
補足:
ドキュメント的にはnpm install --save @nestjs/typeorm typeorm pg
が正解だと思うのですが、下記に記述するマイグレーションファイルが上手く作成できないためバージョンを0.2.45に指定します。
typeORMが3になってから大幅に変更があったみたいで、マイグレーションファイルの作成がうまくできないみたいです。
これは、自分の環境が原因なのか、typeormが原因なのか切り分けができていないので、後で色々試してみないとですね。
NestJSの記事は新しいのがあまりないので情報が少ないのがネックですね。
- DBの設定
dockerでローカルのデータベースを起動します。
dockerが入っていない場合はdockerをインストールしてください。
https://docs.docker.com/get-docker/
プロジェクトの直下に
docker-compose.yaml
を作成し、以下でdocker-compose up -d
をします。
docker-compose.yaml
version: '3.7' services: postgres: image: postgres:12.2-alpine container_name: postgres ports: - 5432:5432 volumes: - ./docker/postgres/init.d:/docker-entrypoint-initdb.d - ./docker/postgres/pgdata:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_INITDB_ARGS: '--encoding=UTF-8' POSTGRES_DB: postgres hostname: postgres restart: always user: root
DBに接続できるかテストしましょう。
以下の状態でnpm run start:dev
をしてapiを起動します。
app.module.ts
import { Module } from '@nestjs/common'; import { TodosModule } from './todos/todos.module'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TodosModule, // 追加 TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', database: 'postgres', autoLoadEntities: true, }), // 追加 ], controllers: [], providers: [], }) export class AppModule {}
問題なければ以下のようになります。
portが違ったりすると以下のようにDBに接続できないエラーが出ます。
Entityを定義
テーブルを作成するためにEntityを定義します。
src > entities > todo.entity.tsを作成する。
todo.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity() export class Todo { @PrimaryGeneratedColumn() id: number; @Column() content: string; @Column() contentType: string; @Column() createAt: string; @Column() updateAt: string; }
DBマイグレーション
マイグレーションを行うことでDBにテーブルを作成していきます。先ほどのEntityからマイグレーションファイルを作成します。
マイグレーションファイルを作成する前にormconfig.jsでtypeormの設定を記述します。これによってマイグレーションファイル作成場所を指定できます。
ormconfig.js
type: 'postgres', username: 'postgres', password: 'postgres', entities: ['dist/entities/*.entity.js'], migrations: ['dist/migrations/*.js'], cli: { entitiesDir: 'src/entities', migrationsDir: 'src/migrations', }, };
- マイグレーションファイルの作成 apiを実行することによってマイグレーションをコンパイルできます。
npx typeorm migration:generate -n CreateTodo
- マイグレーションファイルのコンパイル 作成したマイグレーションファイルをコンパイルします。
npm run start:dev
- マイグレーションの実行 これでtodosテーブルが作成されます。
npx typeorm migration:run
todoList機能の実装
src > todost
直下の以下の3つのフォルダで実装していきます。
todos.controller.ts
todos.module.ts
todos.service.ts
以下のような関係になっています
main.ts
↓
app.module.ts
↓
todo.module.ts
↓ ↓
todo.service.ts ← todo.controller.ts
- バリデーション
先ずは、todoListの機能を作成する前に、リクエストのバリデーションをしたいので、class-validator
とclass-transformer
をインストールします。
npm install --save class-validator class-transformer
- DTO
todos > dto > create-todoを作成し、以下でバリデーションを行います。
stringでないまたは空の場合はリクエストエラーが入るようにしています。
create-todo.dto.ts
import { IsNotEmpty, IsString } from 'class-validator'; export class CreateTodoDto { @IsString() @IsNotEmpty() content: string; @IsString() @IsNotEmpty() contentType: string; }
main.ts
import { ValidationPipe } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' async function bootstrap() { const app = await NestFactory.create(AppModule) app.useGlobalPipes(new ValidationPipe()) //追加 await app.listen(3000) } bootstrap()
- CRUDの実装
以下の機能を実装していきます。
search
create
update
delete
constructor(private readonly todosService: TodosService) {}
をすることでserviceのDIを行っています。
todos.controller.ts
import { Body, Controller, Get, Post, Patch, Delete, Param, } from '@nestjs/common' import { Todo } from 'src/entities/todo.entity' import { CreateTodoDto, SearchTodoDto, UpdateTodoDto, } from './dto/create-todo.dto' import { TodosService } from './todos.service' @Controller('todos') export class TodosController { constructor(private readonly todosService: TodosService) {} @Get() async search(@Body() searchTodoDto: SearchTodoDto): Promise<Todo[]> { return this.todosService.search(searchTodoDto) } @Post() async create(@Body() createTodoDto: CreateTodoDto): Promise<void> { await this.todosService.create(createTodoDto) } @Patch() async update(@Body() updateTodoDto: UpdateTodoDto): Promise<void> { await this.todosService.update(updateTodoDto) } @Delete(':id') async delete(@Param('id') id: number): Promise<void> { await this.todosService.delete(id) } }
serviceの中でconstructor(
@InjectRepository(Todo)
private todosRepository: Repository<Todo>,
) {}
をすることでTypeORMでTodoのDB操作ができるようにしています。
todos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Todo } from 'src/entities/todo.entity' import { Repository } from 'typeorm' import { CreateTodoDto, SearchTodoDto, UpdateTodoDto, } from './dto/create-todo.dto' @Injectable() export class TodosService { constructor( @InjectRepository(Todo) private todosRepository: Repository<Todo>, ) {} async search(searchTodoDto: SearchTodoDto): Promise<Todo[]> { const query = this.todosRepository.createQueryBuilder('todo') if (searchTodoDto.content) { query.where('todo.content like :content', { content: '%' + searchTodoDto.content + '%', }) } if (searchTodoDto.contentType) { query.andWhere('todo.contentType like :contentType', { contentType: '%' + searchTodoDto.contentType + '%', }) } const todo = query.getMany() if (!todo) { throw new NotFoundException() } return todo } async create(createTodoDto: CreateTodoDto): Promise<void> { const { content, contentType } = createTodoDto const todo = { content, contentType, createAt: new Date().toISOString(), updateAt: new Date().toISOString(), } await this.todosRepository.save(todo) } async update(updateTodoDto: UpdateTodoDto): Promise<void> { const todo = await this.todosRepository.findOne(updateTodoDto.id) if (!todo) { throw new NotFoundException() } if (updateTodoDto.content) { todo.content = updateTodoDto.content } if (updateTodoDto.contentType) { todo.contentType = updateTodoDto.contentType } if (updateTodoDto.content || updateTodoDto.contentType) { todo.updateAt = new Date().toISOString() } await this.todosRepository.save(todo) } async delete(id: number): Promise<void> { const todo = await this.todosRepository.findOne(id) if (!todo) { throw new NotFoundException() } await this.todosRepository.delete(todo) } }
実際にpostmanで確認してみましょう。
createを呼んで、
searchを呼ぶと、
先ほどcreateしたtodoが登録されています。
終わりに
NestJSを使用するとかなり簡単にAPI開発ができるなと思いました。
ただ、情報が少ないのが難点ですね、、、 皆さんのお役に立てれば幸いです。