Nodejsフロントエンドとバックエンドの開発(DB編)
はじめに
TypeScriptベースで、Vue + Quasarのフロントエンド、Nodejsのバックエンド構成のSPA型のWebアプリケーションをつくります。
まずはVite + Vue3 + Quasarでフロントエンドを作り、フロントエンドをExpressのバックエンドに組み込んで1ポート配信のWebアプリケーションとして統合します。
前編
からの続きです。
統合編ではフロントエンドと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でインストールします。
MongoDBのインストール
Windows StoreからWSL2のディストリビューションUbuntu-20.04を取得し、初期化を行います。
MongoDBの公式ドキュメントの手順に従いWSL2のUbuntu環境にMongoDBの最新版v7.x.xをインストールします。
以下の入力でインストール完了します。
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
ファイルに以下のレプリカセットの設定を追加します。
...
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
です。
サンプルプログラム
サンプルはここにあります。見た目は前編と同じ簡単なメモアプリです。
統合編のサンプルのバックエンド側にDBでデータをストックする仕組みを追加しました。
使い方
サンプルのルートディレクトリで以下のコマンドを実行します。
yarn install:all
yarn build:all
yarn start
...
server started at http://localhost:3000
ソースの解説
アプリケーションの変更点
統合編のサンプルではバックエンドサーバ内の変数にデータを保持するだけでしたが、今回はDBにデータを保持する方法に変わります。
バックエンドのみの変更でフロントエンドのプログラムの変更はありません。
統合編からの変更点のみを解説します。
追加パッケージ
MongoDBに接続するためにmongoose
パッケージを追加します。
yarn add mongoose
環境変数の型定義
VSCodeでインテリセンス(型推論)が効くようにenv.d.ts
ファイルに型定義を追加します。
# 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
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
でのサーバ起動時に実行します。
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
フィールドが自動的に付加されます。
スキーマのオプションtimestamps
をtrue
に設定するとcreatedAt
(作成日時)やupdatedAt
(更新日時)フィールドが自動的に追加されます。(A1)
プログラムでは_id
とcreatedAt
を利用します。
スキーマのstatics
オプションを使ってモデルにメソッドを追加します。
DBに近い処理はモデル側で処理しコントローラから分離させます。
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
コントローラ
コントローラクラスのメンバーはExpress
のRequestHandler
を返すメンバー関数として定義します。
Requestパラメータを使ってModelのメソッドでDBを操作し、結果をResponseとして返す処理を行います。
B1部ではDBから取得したデータのうち必要なものだけを取り出しています。
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とメソッドにコントローラのメソッドをマッピングします。
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