🐈
【V.9対応】TypeScript +NestJSをプロジェクトで導入したら素晴らしかった件
はじめに
昨年12月に投稿させていただいた記事でしたがNestJS
のバージョンアップに伴いTypeORM
系の処理が動かなくなっていることを発見しましたので記事を更新させていただきました
レポジトリはこちら
環境について
Windows
環境での動作確認はしておりませんのでご了承ください
nest -v
9.1.4
"@nestjs/typeorm": "^9.0.1",
"typeorm": "^0.3.10"
NestJSとは?
Nest (NestJS) は、効率的でスケーラブルなNode.jsサーバサイドアプリケーションを構築するためのフレームワークです。プログレッシブJavaScriptを採用し、TypeScriptを完全にサポートし、OOP(オブジェクト指向プログラミング)、FP(機能的プログラミング)、FRP(機能的反応プログラミング)の要素を兼ね備えています。
環境構築
/nestjs-sample/sample
# NestJSをセットアップするディレクトリを作成します
mkdir nestjs-sample
# cliをインストールします
npm i -g @nestjs/cli
# プロジェクトを作成します
nest new sample
# 今回はnpmでインストールします
? Which package manager would you ❤️ to use? npm
cd sample
npm i
起動するポート番号を変更する
※ 3000番ポートを別のコンテナで利用しているため変更していますが、3000番ポートが空いている場合はこちらの手順は不要です
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3001);
}
bootstrap();
Nestアプリケーションを起動する
/nestjs-sample/sample
npm run start:dev
[8:32:29 PM] Found 0 errors. Watching for file changes.
# 以下のログが出力されると成功しています
[Nest] 95887 - 10/13/2022, 8:32:31 PM LOG [NestFactory] Starting Nest application...
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +710ms
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +8ms
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [RoutesResolver] AppController {/}: +6ms
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [RouterExplorer] Mapped {/, GET} route +5ms
[Nest] 95887 - 10/13/2022, 8:32:32 PM LOG [NestApplication] Nest application successfully started +3ms
CRUD
構築用の雛形を作成する
NestJS CLI
に便利なコマンドが用意されているので利用させていただきます
/nestjs-sample/sample
# 今回はテストは書かないので--no-specでテストファイルを作成しないようにしています
nest g resource users --no-spec
? 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.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.
ORM
のセットアップ
今回は公式がサンプルで使用しているTypeORM
を利用します。
Prisma
を利用する場合は読み替えてください。
/nestjs-sample/sample
npm i --save @nestjs/typeorm typeorm sqlite3
TypeORM
の設定ファイルを作成する
設定について以下の記事を参考にさせていただきました
typeOrm.config.ts
import { DataSource } from 'typeorm';
// 本来は環境変数から取得することを推奨します
export default new DataSource({
type: 'sqlite',
database: 'data/dev.sqlite',
entities: ['dist/**/entities/**/*.entity.js'],
migrations: ['dist/**/migrations/**/*.js'],
logging: true,
});
package.json
を更新する
TypeORM
で利用するコマンドを追加してください。
Windows
環境の場合は$npm_config_name
を%npm_config_name%
としてください。
package.json
"scripts": {
"start:dev": "nest build && nest start --watch",
"typeorm": "ts-node ./node_modules/typeorm/cli",
"typeorm:run-migrations": "npm run typeorm migration:run -- -d ./typeOrm.config.ts",
"typeorm:generate-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:generate ./migrations/$npm_config_name",
"typeorm:create-migration": "npm run typeorm -- migration:create ./migrations/$npm_config_name",
"typeorm:revert-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:revert"
},
app.module
でTypeORM
モジュールを読み込む
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({
type: 'sqlite',
database: 'data/dev.sqlite',
logging: true,
entities: ['dist/**/entities/**/*.entity.js'],
migrations: ['dist/**/migrations/**/*.js'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Entity
を作成する
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 typeorm:generate-migration --name=CreateUser
query: SELECT null as database, null as schema, * FROM "sqlite_master" WHERE "type" = 'table' AND "name" IN ('users')
query: SELECT null as database, null as schema, * FROM "sqlite_master" WHERE "type" = 'index' AND "tbl_name" IN ('users')
query: SELECT * FROM "sqlite_master" WHERE "type" = 'table' AND "name" = 'typeorm_metadata'
Migration /nestjs-demo-rest-api/sample/src/database/migrations/1665664827418-CreateUser.ts has been generated successfully.
マイグレーションの実行
/nestjs-sample/sample
npm run typeorm:run-migrations
query: SELECT * FROM "sqlite_master" WHERE "type" = 'table' AND "name" = 'migrations'
query: CREATE TABLE "migrations" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "timestamp" bigint NOT NULL, "name" varchar NOT NULL)
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 must be executed.
query: BEGIN TRANSACTION
query: CREATE TABLE "users"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" varchar NOT NULL
)
query: INSERT INTO "migrations"("timestamp", "name") VALUES (1665664827418, ?) -- PARAMETERS: ["CreateUser1665664827418"]
Migration CreateUser1665664827418 has been executed successfully.
query: COMMIT
CRUDを実装する
users
モジュールにTypeORM
を読み込む
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 {}
登録したリポジトリをサービスで依存解決する
src/users/users.service.ts
export class UsersService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
) {}
}
バリデーションを実装する
/nestjs-sample/sample
npm i --save class-validator class-transformer
src/users/dto/create-user.dto.ts
import { MaxLength } from 'class-validator';
export class CreateUserDto {
@MaxLength(255, {
message: 'アカウント名は255文字以内で入力してください',
})
name: string;
}
バリデーションをすべてのエンドポイントで有効化する
src/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(3001);
}
bootstrap();
サービスクラスを実装する
サービスクラス
src/users/users.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
@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> {
return await this.userRepository.findOneBy({
id: id,
});
}
/**
* 更新
* @param id
* @param updateUserDto
* @returns
*/
async update(
id: number,
updateUserDto: UpdateUserDto,
): Promise<{ message: string }> {
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 }> {
await this.userRepository.delete(id).catch((e) => {
throw new InternalServerErrorException(
`[${e.message}]アカウントID「${id}」の削除に失敗しました。`,
);
});
return { message: `アカウントID「${id}」の削除に成功しました。` };
}
}
サービスクラスをコントローラーから呼び出す
コントローラークラス
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);
}
}
RestAPI
にリクエストを投げてみる
# 登録
curl -XPOST -H "Content-Type:application/json" localhost:3001/users -d '{"name":"サンプル太郎"}'
{"message":"アカウントの登録に成功しました"}%
# 全件取得
curl -H "Content-Type:application/json" loca
lhost:3001/users
[{"name":"サンプル太郎","id":1}]%
# 更新
curl -XPATCH -H "Content-Type:application/js
on" localhost:3001/users/1 -d '{"name":"更新したよ"}'
{"message":"アカウントID「1」の更新に成功しました。"}%
# 個別取得
curl -H "Content-Type:application/json" localhost:3001/users/1
{"name":"更新したよ","id":1}%
# 削除
curl -XDELETE -H "Content-Type:application/j
son" localhost:3001/users/1
{"message":"アカウントID「1」の削除に成功しました。"}%
Discussion