NestJSでMongooseやTypeORMを使わずにMongoDBにアクセスするシンプルなAPIを作ってみた
はじめに
NestJS の公式ドキュメントを見ると MongoDB の接続方法としてMongoose 使った方法とTypeORM を使った方法が紹介されているけど、残念ながらMongoDB Node Driver (mongodb)を使った方法は書かれてない。どうも TypeORM の MongoDB 対応はまだまだ微妙な感じだし、TypeScript 使うなら Mongoose 必要ない気もする。ということで、試行錯誤しながら、シンプルにMongoDB Node Driver (mongodb)のみで NestJS から MongoDB にアクセスしてみた。ちゃんと動くか確認するために、CRUD の API を一通り作って試してみた。
試すとき、下記サイトをすごく参考にした。ただ、そのままじゃ動かなかったので色々修正した。
NestJS with MongoDB native driver | by Gustavo Oliveira | Medium
フルのコードはここに置いた。
GitHub - optimisuke/hello-nestjs-puremongo
コード
DB 接続側から主要な部分を説明していく。
DB 接続
まず、useFactory
を使って、MongoDB に接続してDb
を作るモジュールを準備する。useFactory
は動的に provider を作ってくれる。認証は省略。
import { Module } from '@nestjs/common';
import { MongoClient, Db } from 'mongodb';
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (): Promise<Db> => {
try {
const client = await MongoClient.connect('mongodb://127.0.0.1');
return client.db('test');
} catch (e) {
throw e;
}
},
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}
userFactory
使うような Provider は、ここ参照。
Custom providers | NestJS - A progressive Node.js framework
サービス周辺
次に、先ほどのDb
を使ってデータベースにデータを書き込んだり読み込んだりする部分。リポジトリ層を挟んでも良いのかもだけど省略。
ポイントは、Db
をコンストラクタで Dependency injection してる点と、ジェネリクスで User Interface を指定したコレクションを使ってる点。(インターフェースは型にしても良いのかも)
エラーはそんなに考えてないけど、NotFoundException()
は throw してみた。Mongo のエラーをハンドリングしたい時はここを参照。
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { User } from './interfaces/user.interface';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';
import { Collection, Db, ObjectId } from 'mongodb';
@Injectable()
export class UsersService {
collectionUser: Collection<User>;
constructor(
@Inject('DATABASE_CONNECTION')
private db: Db,
) {
this.collectionUser = db.collection<User>('users');
}
async find(): Promise<User[]> {
return await this.collectionUser.find().toArray();
}
async findOne(id: string): Promise<User> {
const response = await this.collectionUser.findOne({
_id: new ObjectId(id),
});
if (!response) {
throw new NotFoundException();
}
return response;
}
async create(body: CreateUserDto): Promise<void> {
await this.collectionUser.insertOne(body);
}
async update(id: string, body: UpdateUserDto): Promise<User> {
const response = await this.collectionUser.findOneAndUpdate(
{
_id: new ObjectId(id),
},
{
$set: {
...body,
},
},
{
returnDocument: 'after',
},
);
if (!response.ok) {
throw new NotFoundException();
}
return response.value;
}
async delete(id: string): Promise<void> {
const response = await this.collectionUser.deleteOne({
_id: new ObjectId(id),
});
if (response.deletedCount === 0) {
throw new NotFoundException();
}
}
}
User Interface はこんな感じ。_id
をオプションにして、ObjectId 型にしてる。
import { ObjectId } from 'mongodb';
export interface User {
_id?: ObjectId;
name: string;
email: string;
age: number;
}
データベースにアクセスする部分はこんなもん。
コントローラー・モジュール周辺
残りのコントローラーとモジュールは大したことしてないけど、一応記載しておく。
まず、コントローラー。Create を Post, Read を Get, Update を Put, Delete を Delete で書いた。パスパラメータを@Param
、ボディを@Body
で記載。バリデーションを DTO (Data Transfer Object)を使って記載。
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';
import { User } from './interfaces/user.interface';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() createUserDto: CreateUserDto) {
await this.usersService.create(createUserDto);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(id, updateUserDto);
}
@Get()
async findAll(): Promise<User[]> {
return this.usersService.find();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.usersService.delete(id);
}
}
DTO はこんな感じ。NestJS はバリデーションをシンプルにかける。楽ちん。
import { IsEmail, IsNotEmpty, IsInt } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
name: string;
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsInt()
age: number;
}
import { IsEmail, IsNotEmpty, IsInt } from 'class-validator';
export class UpdateUserDto {
@IsNotEmpty()
name: string;
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsInt()
age: number;
}
モジュールはこんな感じ。DatabaseModule
をインポートしてる。
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
その他
残りの下記ファイル等は、GitHub - optimisuke/hello-nestjs-puremongoに置いてるので、そっち参照。.http
はREST Client用のファイル。API の動作確認に使用。
- main.ts
- docker-compose.yml
- .http
試し方
こんな感じで動くはず。git と Node.js と Docker が動く前提。
git clone https://github.com/optimisuke/hello-nestjs-puremongo
cd hello-nestjs-puremongo
npm install
docker compose up -d
npm run start:dev
あとは、Postman とかREST Clientとかでいい感じに API を呼び出す。API の仕様は、コードか.http
ファイル参照。REST Clientを使う場合は、.http
ファイルを使って試せる。すごく便利なので是非。
MongoDB にデータが書き込まれてるかは、MongoDB Compass等を使って確認。
こんな感じで見れるはず。
おわりに
それっぽく MongoDB に接続できて満足。ディレクトリ構造はもうちょい考えても良いかもだけどいい感じ。
基本的に、依存するライブラリやフレームワークはできるだけ少ない方が良いと思うので、NestJS を使うとしても Mongoose や TypeORM を使わずに MongoDB にアクセスできるようになることは良いことだと思う。
NestJS と MongoDB 完全に理解したと言いたいとこだけど、NestJS も MongoDB もまだまだよくわかってないので引き続き試行錯誤していきたい。
Discussion