🚀

Nodejsフロントエンドとバックエンドの開発(DB編)

2024/03/18に公開

はじめに

TypeScriptベースで、Vue + QuasarのフロントエンドNodejsのバックエンド構成のSPA型のWebアプリケーションをつくります。
まずはVite + Vue3 + Quasarでフロントエンドを作り、フロントエンドをExpressのバックエンドに組み込んで1ポート配信のWebアプリケーションとして統合します。

前編

  1. Nodejsフロントエンドとバックエンドの開発(Quasarフロントエンド編)
  2. Nodejsフロントエンドとバックエンドの開発(統合編)

からの続きです。

統合編ではフロントエンドDBなしのバックエンドを統合しました。
今回は、前回やり残していたバックエンドへのDB機能の組み込みを解説します。
主にDBの環境構築プログラムの改造点についての説明です。

開発環境

今回の開発で必要な環境は、Node.jsの実行環境とMongoDBです。

  • Windows 11
  • Visual Studio Code
  • Node.js v20(Windows Installerでインストール)
  • MongoDB(WSL2)

ローカルの開発環境なので、ここではプログラム生産性重視の方針で開発環境を選択します。
ハードウェアの性能を最大限に活かすために、OSネイティブに近い環境や出来るだけ軽い仮想環境を使用します。

Windows上で直接できるNodeの実行環境をインストールします。再インストールでバージョンアップが手軽にできます。
MongoDBもWindowsで直接実行できる環境がありますが、バージョンアップやLinuxコマンドの実行、起動しやすさなどを考慮するとWSL2環境の方が扱いやすいです。
よって以下のような開発環境になります。

環境構築

サンプルの動作する環境を構築します。

Node.js環境のインストール

Windows Installerでインストールします。

https://nodejs.org/en/download

MongoDBのインストール

Windows StoreからWSL2のディストリビューションUbuntu-20.04を取得し、初期化を行います。

MongoDBの公式ドキュメントの手順に従いWSL2のUbuntu環境にMongoDBの最新版v7.x.xをインストールします。

https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/

以下の入力でインストール完了します。

sudo apt-get install gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
   sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg \
   --dearmor

echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org

WSL2起動時に自動起動するように、起動ユーザの.bash_profileに以下を追加します。

wsl.exe -d $WSL_DISTRO_NAME -u root systemctl status mongod > /dev/null || wsl.exe -d $WSL_DISTRO_NAME -u root systemctl start mongod

サービスの起動

ターミナルのメニューからUbuntuを選択しWSL2を起動するとMongoDBも起動されます。

コマンドでMongoDBの起動状態を確認します。
起動できていない場合は再度ターミナルから起動してみます。

naoki@DESKTOP-OSHVO2O:~$ systemctl status mongod
● mongod.service - MongoDB Database Server
     Loaded: loaded (/lib/systemd/system/mongod.service; disabled; vendor preset: enabled)
     Active: active (running) since Tue 2024-02-27 07:32:58 JST; 4s ago
       Docs: https://docs.mongodb.org/manual
   Main PID: 909 (mongod)
     Memory: 47.0M
     CGroup: /system.slice/mongod.service
             └─909 /usr/bin/mongod --config /etc/mongod.conf

Feb 27 07:32:58 DESKTOP-OSHVO2O systemd[1]: Started MongoDB Database Server.

MongoDB初期化

サンプルを動作させるには必ずしも必要ではありませんが、Prisma等、後々のサンプルで必要になるのでMongoDBの初期化を行います。

MongoDBの設定ファイル/etc/mongo/confファイルに以下のレプリカセットの設定を追加します。

mongo.conf
...
replication:
  replSetName: rs0
...

mongoshコマンドでMongoDBに入って、レプリカセットの初期化コマンドを実行します。

mongosh
test> rs.initiate()
test> quit

VSCode拡張機能

VSCodeに拡張機能「MongoDB for VS Code」を入れておくとDBのデータが参照できます。
設定は何もしなくてもこのままでVSCodeからMongoDBが利用できる状態になります。
接続URIはmongodb://127.0.0.1:27017です。

https://marketplace.visualstudio.com/items?itemName=mongodb.mongodb-vscode

サンプルプログラム

サンプルはここにあります。見た目は前編と同じ簡単なメモアプリです。
統合編のサンプルのバックエンド側にDBでデータをストックする仕組みを追加しました。

https://github.com/czbone/express-db-frontend-combined

使い方

サンプルのルートディレクトリで以下のコマンドを実行します。

yarn install:all
yarn build:all
yarn start
...
server started at http://localhost:3000

ソースの解説

アプリケーションの変更点

統合編のサンプルではバックエンドサーバ内の変数にデータを保持するだけでしたが、今回はDBにデータを保持する方法に変わります。
バックエンドのみの変更でフロントエンドのプログラムの変更はありません。
統合編からの変更点のみを解説します。

追加パッケージ

MongoDBに接続するためにmongooseパッケージを追加します。

yarn add mongoose

環境変数の型定義

VSCodeでインテリセンス(型推論)が効くようにenv.d.tsファイルに型定義を追加します。

.env
# Set to production when deploying to production
NODE_ENV=development

# Node.js server configuration
SERVER_PORT=3000
HOST_URL=http://localhost:3000

# Database configuration
MONGO_URI=mongodb://127.0.0.1:27017/sample-NoteDB
src/env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: string
    readonly SERVER_PORT: string
    readonly HOST_URL: string
    readonly MONGO_URI: string
  }
}

Mongoose初期化

Mongooseの初期処理です。index.tsでのサーバ起動時に実行します。

db/mongoDB.ts
import mongoose from 'mongoose'

export const initDB = async () => {
  // MongoDB初期接続
  try {
    mongoose.set('strictQuery', false)
    await mongoose.connect(process.env.MONGO_URI)

    console.log('[MongoDB] connection established.')
  } catch (err) {
    console.error('[MongoDB]', err)
    throw new Error('[MongoDB] Critical System error.')
  }
}

API呼び出し

APIの呼び出しはルーターコントローラモデルの順に実行されます。
DBに近いMongooseのモデルから順に説明します。

モデル

MongooseはTypescriptに最適化されていません。
MongooseをTypescriptに厳密に合わせると型定義だらけの見通しの悪いコードになるので、ほどほどの型定義にとどめます。
いずれMongooseがTypeScriptに最適化するのを待ちます。

Mongooseのスキーマを使用してモデルを定義します。
スキーマを定義すると、キーになる_idフィールドが自動的に付加されます。
スキーマのオプションtimestampstrueに設定するとcreatedAt(作成日時)やupdatedAt(更新日時)フィールドが自動的に追加されます。(A1)
プログラムでは_idcreatedAtを利用します。

スキーマstaticsオプションを使ってモデルメソッドを追加します。
DBに近い処理モデル側で処理しコントローラから分離させます。

model/note.ts
import mongoose from 'mongoose'

const schema = new mongoose.Schema(
  {
    message: { type: String, required: true, trim: true }
  },
  {
    timestamps: true, // createdAt,updatedAtのタイムスタンプ追加 [A1]
    statics: {
      async getNotes(): Promise<any> {
        try {
          const notes = await Note.find().sort({ createdAt: 1 })
          return notes
        } catch (err) {
          console.error('DBエラーが発生しました')
          return null
        }
      },
      async deleteNote(id: string): Promise<any> {
        try {
          const note = await Note.deleteOne({ _id: id })
          return note
        } catch (err) {
          console.error('DBエラーが発生しました')
          return null
        }
      },
      async addNote(message: string): Promise<any> {
        try {
          const note = await Note.create({
            message: message
          })
          return note
        } catch (err) {
          console.error('DBエラーが発生しました')
          return null
        }
      }
    }
  }
)

const Note = mongoose.model('Note', schema, 'note' /* MongoDBのコレクション名 */)

export default Note

コントローラ

コントローラクラスのメンバーはExpressRequestHandlerを返すメンバー関数として定義します。
Requestパラメータを使ってModelのメソッドでDBを操作し、結果をResponseとして返す処理を行います。
B1部ではDBから取得したデータのうち必要なものだけを取り出しています。

controller/note.ts
import { Note } from '@/models'
import { NextFunction, Request, RequestHandler, Response } from 'express'

type NewNote = {
  message: string
}
type ResponseNote = {
  id: string
  message: string
}
type NoteDoc = {
  _id: string
  message: string
}

class NoteController {
  list: RequestHandler = async (req: Request, res: Response, _next: NextFunction) => {
    const notes: NoteDoc[] = await Note.getNotes()
    if (notes) {
      // クライアントに返すデータを制限
      const responseNotes: ResponseNote[] = notes.map((note) => ({
        id: note._id,
        message: note.message
      }))  // [B1]
      res.json(responseNotes)
    } else {
      res.status(400).json({ message: 'データ取得に失敗しました' })
    }
  }
  create: RequestHandler = async (req: Request, res: Response, _next: NextFunction) => {
    const { message } = req.body as NewNote

    const note = await Note.addNote(message)
    if (note) {
      res.json({ message: note.message as string, id: note._id as string } as ResponseNote)
    } else {
      res.status(400).json({ message: 'データ登録に失敗しました' })
    }
  }
  delete: RequestHandler = async (req: Request, res: Response, _next: NextFunction) => {
    const noteId = req.params.id

    const note = await Note.deleteNote(noteId)
    if (note) {
      res.json()
    } else {
      res.status(400).json({ message: 'データ削除に失敗しました' })
    }
  }
}
export default new NoteController()

ルーティング

ルーターではリクエストのURLとメソッドにコントローラのメソッドをマッピングします。

router/note.ts
import { NoteController } from '@/controllers'
import { Router } from 'express'

const router = Router()

router.get('/', NoteController.list) // 一覧取得
router.post('/', NoteController.create) // 登録
router.delete('/:id', NoteController.delete) // 削除

export default router

おわりに

サーバをDBに連結し、API処理を追加するだけで本格的なWebアプリケーションになってきました。
サンプルは非常に単純ですが、実践的なインターフェイスに設計しています。
参考にしてみてください。

Discussion