🐈‍⬛

NestJSでMongooseやTypeORMを使わずにMongoDBにアクセスするシンプルなAPIを作ってみた

2023/01/09に公開

はじめに

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 を作ってくれる。認証は省略。

database.module.ts
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 のエラーをハンドリングしたい時はここを参照。

users.service.ts
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 型にしてる。

user.interface.ts
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)を使って記載。

users.controller.ts
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 はバリデーションをシンプルにかける。楽ちん。

create-user.dto.ts
import { IsEmail, IsNotEmpty, IsInt } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  name: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsInt()
  age: number;
}

update-user.dto.ts
import { IsEmail, IsNotEmpty, IsInt } from 'class-validator';

export class UpdateUserDto {
  @IsNotEmpty()
  name: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsInt()
  age: number;
}

モジュールはこんな感じ。DatabaseModuleをインポートしてる。

users.module.ts
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に置いてるので、そっち参照。.httpREST 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 もまだまだよくわかってないので引き続き試行錯誤していきたい。

GitHubで編集を提案

Discussion