Nest.js+TypeORM+PostgreSQLで実装してみた
概要
最近、業務でNest.jsを使用しており、理解を深めるために、Nest.jsとTypeScriptのライブラリであるTypeORMを使ってデータベース操作を実践してみたいと思います。内容が少し複雑で難しい部分があるため、できる限り解説を挟んでいきますが、不十分な説明や知識不足の箇所があるかもしれません。その点、ご了承いただけると幸いです。
機能
Nest.js
- TypeScriptベースでNode.jsのフレームワーク
- モジュール構造
https://nestjs.com/
TypeORM
- クエリを直接書く必要がない(今回はベタ書きします)
- エンティティ
- DBマイグレーション
https://typeorm.io/
Nest.jsの導入
以下をターミナルで実行します。インストールが完了したらnest
コマンドが使えるようになります。
npm i -g @nestjs/cli
その後新規プロジェクトを立ち上げます。今回は例としてTodo機能を追加するので名前はTodo_project
とします。
nest new Todo_project
TypeORMの導入
TypeORMのインストールを実行します。pg
はPostgreSQLの略です。DBとの接続に必要です。
npm install @nestjs/typeorm typeorm pg
TypeORMの接続先は以下です。
synchronizeをtrueに設定すると、アプリケーションが起動するたびにエンティティ定義に基づいて自動的にデータベースのテーブル構造を更新します。本番環境では通常 false にします。
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DATABASE_HOST'),
port: configService.get<number>('DATABASE_PORT'),
username: configService.get<string>('DATABASE_USER'),
password: configService.get<string>('DATABASE_PASSWORD'),
database: configService.get<string>('DATABASE_NAME'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: true,
}
.env
ファイルには以下のようなDB接続情報を設定します。
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres_user
DATABASE_PASSWORD=secret_password
DATABASE_NAME=my_database
Nest.jsの構成要素作成
Todo.moduleの記述
module
とはアプリケーションの構成を管理するための要素です。Nest.jsではすべての機能がmodule
に含まれています。module
は、関連するcontroller
やservice
をグループ化し、1つのまとまりとして管理します。
TodoController
,TodoService
,PostTodo
をimportしmodule
に定義します。
このmodule
はTodoアプリケーションの機能を提供するために必要なコンポーネントを登録し、データベースへの接続設定などを行います。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';
import { PostTodo } from './dto/todo.table';
@Module({
imports: [
TypeOrmModule.forFeature([PostTodo]),
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DATABASE_HOST'),
port: configService.get<number>('DATABASE_PORT'),
username: configService.get<string>('DATABASE_USER'),
password: configService.get<string>('DATABASE_PASSWORD'),
database: configService.get<string>('DATABASE_NAME'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: true,
}),
}),
],
controllers: [TodoController],
providers: [TodoService],
})
export class AppModule {}
Todo.controllerの記述
Get
,Post
リクエストを呼び出し、エンドポイントを提供しています。これらはTodoService
を利用してバックエンドのデータ操作を行い、クライアントからのリクエストに対してTodoの作成、取得、編集、削除を行います。
TodoService
とPostTodo
をimportします。
import { PostTodo } from './dto/todo.table';
import { TodoService } from './todo.service';
import {
Body,
Controller,
Get,
HttpCode,
Post,
Delete,
Param,
} from '@nestjs/common';
@Controller('todo')
export class TodoController {
constructor(private readonly appService: TodoService) {}
@Get('getList')
async getList(): Promise<PostTodo[]> {
return this.appService.getList();
}
@Post('registration')
@HttpCode(201)
async create(@Body() post: Omit<PostTodo, 'id'>): Promise<void> {
await this.appService.create(post);
}
@Post('edit/:id')
async edit(
@Param('id') id: string,
@Body() updatedPost: PostTodo,
): Promise<string> {
return this.appService.edit(id, updatedPost);
}
@Delete('delete/:id')
async delete(@Param('id') id: string): Promise<string> {
return this.appService.delete(id);
}
@Post('createTable')
async createTable(): Promise<void> {
console.log('create完了');
await this.appService.createTable();
}
}
Todo.serviceの記述
service
とは、ビジネスロジックやDBとのやり取りなど、具体的な処理を行う部分です。外部APIの呼び出しなどを行います。
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { PostTodo } from './dto/todo.table';
@Injectable()
export class TodoService {
constructor(
@InjectEntityManager() private readonly entityManager: EntityManager,
) {}
async createTable(): Promise<void> {
try {
await this.entityManager.query(`
CREATE TABLE post_todo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task VARCHAR(255) NOT NULL,
detail TEXT NOT NULL,
isToggle BOOLEAN DEFAULT false,
);
`);
} catch (e) {
console.error(e);
}
}
async getList(): Promise<PostTodo[]> {
try {
const res = await this.entityManager.query('SELECT * FROM post_todo');
return res;
} catch (e) {
console.error(e);
}
}
async create(post: Omit<PostTodo, 'id'>): Promise<void> {
try {
await this.entityManager.query(
`INSERT INTO post_todo (task, detail, "isToggle") VALUES ($1, $2, $3)`,
[post.task, post.detail, post.isToggle],
);
} catch (e) {
console.error(e);
}
}
async edit(id: string, updatedPost: PostTodo): Promise<string> {
try {
const post = await this.entityManager.query(
`SELECT * FROM post_todo WHERE id = $1`,
[id],
);
if (post.length === 0) {
return 'Post not found';
}
await this.entityManager.query(
`UPDATE post_todo SET task = $1, detail = $2, "isToggle" = $3 WHERE id = $4`,
[updatedPost.task, updatedPost.detail, updatedPost.isToggle, id],
);
return 'Post updated successfully';
} catch (e) {
console.error(e);
}
}
async delete(id: string): Promise<string> {
try {
const post = await this.entityManager.query(
`SELECT * FROM post_todo WHERE id = $1`,
[id],
);
if (post.length === 0) {
return 'Post not found';
}
await this.entityManager.query(`DELETE FROM post_todo WHERE id = $1`, [
id,
]);
return 'Post deleted successfully';
} catch (e) {
console.error(e);
}
}
}
ここで一つポイントですがcreateTable
メソッド内でentityManager.query
というメソッドがあります。これはTypeORMのEntityManager
クラスを使用してデータベースに対して生のSQLクエリを実行するためのメソッドです。これを使う理由は特定のEntity
に依存せずにカスタムSQLクエリを直接実行したいときに使用されます。
個人の見解としてはメンテナンス性、セキュリティの観点からこの書き方はあまり推奨はしませんが、複雑なクエリやパフォーマンス向上が必要な場合に便利なので今回はこの書き方で実行します。
async createTable(): Promise<void> {
try {
await this.entityManager.query(`
CREATE TABLE post_todo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task VARCHAR(255) NOT NULL,
detail TEXT NOT NULL,
isToggle BOOLEAN DEFAULT false,
);
`);
} catch (e) {
console.error(e);
}
}
Todo.table(Entity)の記述
このEntity
はTypeORMを使用して定義されたもので、クラスフィールドはテーブルのカラムに対応しており、CRUD操作に使用されます。
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('post_todo')
export class PostTodo {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255 })
task: string;
@Column({ type: 'text' })
detail: string;
@Column({ type: 'boolean', default: false })
isToggle: boolean;
}
mainの記述
今回はlocalhost:3011
で開きます。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './todo.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3011);
}
bootstrap();
apiの実装
今回はシンプルなローカル環境で実装するのでPostman
を使って実装します。
Post
リクエストを選択しhttp://localhost:3011/todo/createTable
をPost
します。コンソールにcreate完了と出力されたらTableができます。
次に先ほどの同じようにPost
リクエストを選択しhttp://localhost:3011/todo/registration
を書き、JSON形式でBODYに
{
"task": "買い物をする",
"detail": "パン、野菜",
"isToggle": false
}
と記述しPost
リクエストをします。
その後Get
リクエストを選択しhttp://localhost:3011/todo/getList
を記述しリクエストを送ると先ほどリクエストしたtodoがレスポンスとして返ってきます。この時のidはuuid
なのでランダムで生成されます。
このようにレスポンスが返ってきたら実装完了です!
最後に
バックエンドでの実装では理解に苦しみましたがなんとか記事にすることができました。正直フロントエンドよりバックエンドでコード書いてる方が個人的には好きです。
間違いや不適切な部分がございましたらご指摘いただけると嬉しいです。
Discussion