NestJSをTypeORM 0.3 で使う
NestJS と TypeORM の学習をしていたのですが、Web には TypeORM 0.2
系の情報が多く、0.3
系を使うと色々とはまったので、簡単な CRUD の REST API を作るまでをまとめました。
今回作成したコードは github にあります。
PostgreSQL の構築
TypeORM は様々な DB に対応していますが、今回は PostgreSQL を使用します。docker-compose で起動できるようにします。
version: '3'
services:
postgres:
image: postgres:14
volumes:
- ./postgres/data:/var/lib/postgresql/data
- ./postgres/initdb:/docker-entrypoint-initdb.d
ports:
- "${POSTGRES_PORT}:5432"
environment:
- TZ=Asia/Tokyo
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
postgres:
driver: local
.env
を作成して、設定情報を記載します。
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_DB=nestjs
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changeme
ここまでで、いったん、PostgreSQL が起動することを確認します。
docker-compose up
アプリケーション作成
NestJS アプリを作成します。
nest new webapp
cd webapp
開発モードで起動します。
npm run start:dev
http://localhost:3000/ にアクセスし、Hello World!
と表示されれば OK です。
TypeORM の導入
TypeORM に必要なパッケージをインストールします。
npm install @nestjs/typeorm typeorm reflect-metadata pg
PostgreSQL の設定情報を環境変数から読み込むために、 @nestjs/config
と、バリデーションを行うための Joi
をインストールします。
npm install @nestjs/config joi
0.2 では ormconfig.ts
に接続オプションを記述していたようですが、0.3 では非推奨になったようです。
以下のように、モジュールを作成して、 app.module.ts
でインポートします。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('POSTGRES_HOST'),
port: configService.get('POSTGRES_PORT'),
username: configService.get('POSTGRES_USER'),
password: configService.get('POSTGRES_PASSWORD'),
database: configService.get('POSTGRES_DB'),
entities: [],
synchronize: true,
})
}),
],
})
export class DatabaseModule { }
ここまでで、PostgreSQL と NestJS アプリを起動して動作確認してみます。
$ docker-compose up -d
$ cd webbapp
$ npm run start:dev
[1:12:06 PM] Starting compilation in watch mode...
[1:12:12 PM] Found 0 errors. Watching for file changes.
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [NestFactory] Starting Nest application...
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] DatabaseModule dependencies initialized +140ms
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] ConfigHostModule dependencies initialized +2ms
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] ConfigModule dependencies initialized +2ms
[Nest] 17171 - 08/16/2022, 1:12:16 PM LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 17171 - 08/16/2022, 1:12:17 PM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +934ms
[Nest] 17171 - 08/16/2022, 1:12:17 PM LOG [RoutesResolver] AppController {/}: +8ms
[Nest] 17171 - 08/16/2022, 1:12:17 PM LOG [RouterExplorer] Mapped {/, GET} route +4ms
[Nest] 17171 - 08/16/2022, 1:12:17 PM LOG [NestApplication] Nest application successfully started +5ms
リソースの作成
CRUD generator でリソースのひな形を作成します。
$ nest g resource cats
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/cats/cats.controller.spec.ts (556 bytes)
CREATE src/cats/cats.controller.ts (873 bytes)
CREATE src/cats/cats.module.ts (240 bytes)
CREATE src/cats/cats.service.spec.ts (446 bytes)
CREATE src/cats/cats.service.ts (595 bytes)
CREATE src/cats/dto/create-cat.dto.ts (29 bytes)
CREATE src/cats/dto/update-cat.dto.ts (165 bytes)
CREATE src/cats/entities/cat.entity.ts (20 bytes)
UPDATE package.json (2156 bytes)
UPDATE src/app.module.ts (788 bytes)
✔ Packages installed successfully.
Entity の作成
エンティティーの作成を行います。
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Timestamp,
UpdateDateColumn,
} from 'typeorm';
@Entity('cats')
export class Cat {
@PrimaryGeneratedColumn({
name: 'id',
unsigned: true,
type: 'smallint',
comment: 'ID',
})
readonly id: number;
@Column('varchar', { comment: '猫の名前' })
name: string;
@CreateDateColumn({ comment: '登録日時' })
readonly created_at?: Timestamp;
@UpdateDateColumn({ comment: '最終更新日時' })
readonly updated_at?: Timestamp;
constructor(name: string) {
this.name = name;
}
}
database.modlue.ts
に Cat
を追加します。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Cat } from './cats/entities/cat.entity'; // <== 追加
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('POSTGRES_HOST'),
port: configService.get('POSTGRES_PORT'),
username: configService.get('POSTGRES_USER'),
password: configService.get('POSTGRES_PASSWORD'),
database: configService.get('POSTGRES_DB'),
entities: [
// '**/entities/*.ts', // It cases "SyntaxError: Cannot use import statement outside a module"
Cat, // <== 追加
],
synchronize: true,
})
}),
],
})
export class DatabaseModule { }
Database にテーブルが作られていることが確認できます。
Module の作成
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './entities/cat.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Cat]),
],
exports: [TypeOrmModule],
controllers: [CatsController],
providers: [CatsService]
})
export class CatsModule { }
バリデーション
依存関係をインストールします。
npm i --save class-validator class-transformer
バリデーションクラスを実装していきます。
import { IsNotEmpty, MaxLength } from 'class-validator';
export class CreateCatDto {
@IsNotEmpty({ message: '名前は必須項目です' })
@MaxLength(255, { message: '名前は255文字以内で入力してください' })
name: string;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateCatDto } from './create-cat.dto';
import { IsNotEmpty, MaxLength } from 'class-validator';
export class UpdateCatDto extends PartialType(CreateCatDto) {
@IsNotEmpty({ message: '名前は必須項目です' })
@MaxLength(255, { message: '名前は255文字以内で入力してください' })
name: string;
}
main.ts
にバリデーションを適用する処理を追加します。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // <== 追加
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // <== 追加
await app.listen(3000);
}
bootstrap();
Controller の実装
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { DeleteResult, UpdateResult } from 'typeorm';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) { }
@Post()
async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
return await this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return await this.catsService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Cat> {
return await this.catsService.findOne(+id);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() updateCatDto: UpdateCatDto,
): Promise<Cat> {
return await this.catsService.update(+id, updateCatDto);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<DeleteResult> {
return await this.catsService.remove(+id);
}
}
Service の実装
import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Repository, UpdateResult } from 'typeorm';
@Injectable()
export class CatsService {
constructor(@InjectRepository(Cat) private catRepository: Repository<Cat>) { }
/**
* @summary 登録機能
* @param createCatDto
*/
async create(createCatDto: CreateCatDto): Promise<Cat> {
return await this.catRepository
.save({ name: createCatDto.name })
.catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
/**
* @summary 全件取得
*/
async findAll(): Promise<Cat[]> {
return await this.catRepository.find().catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
/**
* @summary 該当ID取得
* @param id
*/
async findOne(id: number): Promise<Cat> {
const cat = await this.catRepository.findOneBy({ id });
if (!cat) {
throw new NotFoundException(
`${id}に一致するデータが見つかりませんでした。`,
);
}
return cat;
}
/**
* @summary 該当ID更新
* @param id
*/
async update(id: number, updateCatDto: UpdateCatDto): Promise<Cat> {
{
await this.catRepository
.update(id, { name: updateCatDto.name })
.catch((e) => {
throw new InternalServerErrorException(e.message);
});
const updatedPost = await this.catRepository.findOneBy({ id });
if (updatedPost) {
return updatedPost
}
throw new NotFoundException('${id}に一致するデータが見つかりませんでした。');
}
}
/**
* @summary 削除
* @param id
*/
async remove(id: number): Promise<DeleteResult> {
const response = await this.catRepository.delete(id).catch((e) => {
throw new InternalServerErrorException(e.message);
});
if (!response.affected) {
throw new NotFoundException(`${id} was not found`);
}
return response;
}
}
動作確認
登録
$ curl --location --request POST 'localhost:3000/cats' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "tama"}'
全件取得
curl --location --request GET 'localhost:3000/cats' \
--header 'Content-Type: application/json'
一件取得
curl --location --request GET 'localhost:3000/cats/1' \
--header 'Content-Type: application/json'
更新
curl --location --request PATCH 'localhost:3000/cats/1' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "pochi"}'
削除
curl --location --request DELETE 'localhost:3000/cats/1' \
--header 'Content-Type: application/json'
Discussion