本文書では、Express.jsからNoSQLデータベースであるMongoDBへの接続方法を確認します。MongoDBへの接続完了後、Visual Basic Studioの拡張機能REST Clientを利用してGET, PATCH, DELETEのHTTPリクエストを送信し、データの作成、更新、削除を行います。

動作確認は、MAC環境で行なっており、node.jsを事前にインストールしておく必要があります。またデータベースへのHTTPリクエストにはVisual Studio Codeの拡張機能REST Clientを利用します。REST Clientの基本的な使い方も理解することができます。

MongoDBのインストール

Homebrewを利用してMongoDBのインストールを行います。MAC環境へのMongoDBへのインストール方法については、下記の文書を参考に行なってください。

Express.jsのインストール

Express.jsのインストールを行います。インストールを行うディレクトリは任意の名前をつけてください。ここではexpress_mongodbという名前で作成します。


$ mkdir express_mongodb
$ cd express_mongodb/

Express.jsで使用するパッケージ管理を行うためnpm initコマンドでpackage.jsonファイルの作成を行います。-yオプションをついて対話モードをスキップします。


$ npm init -y

npmコマンドを実行したexpress_mongodbディレクトリにpackage.jsonファイルが作成されます。

次にnpmコマンドでexpress.jsのインストールを行います。


 $ npm install express

Express.jsの動作確認

Hello Worldの表示

Express.jsのインストールが完了したので、動作確認を行うためにアクセスしてきたブラウザにHello Worldを表示させます。

express_mongodbディレクトリの中にapp.jsファイルを作成して、下記を記述してください。


const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

app.jsファイル作成後、express_mongodbディレクトリで下記のnodeコマンドを実行してください。


$ node app.js
Example app listening on port 3000!

ブラウザでlocalhostの3000ポートにアクセスするとブラウザにHello World!が表示されれば、Express.jsは正常に動作しています。

Hello Worldがブラウザに表示
Hello Worldがブラウザに表示

nodemonのインストール

app.jsファイルの更新を監視するためにnodemonをインストールします。nodemonによりapp.jsファイルの更新を実行するとnodeコマンドが自動で再実行されるようになります。


$ npm install nodemon
nodemonを利用しない場合は、app.jsファイルを更新する度に手動でnodeコマンドを再実行する必要があります。

インストール後、下記のコマンドを実行してnodemonでapp.jsファイルの更新を監視します。


 $ npx nodemon app.js

動作確認中は基本的に上記のコマンドを常時実行しておきます。

Mongooseのインストール

MongooseはNode.jsからMongoDBを操作するために利用するObject Data Modeling (ODM) ライブラリです。

MongoDBではリレーショナルデータベースのようにスキーマが必要ではありませんが、Mongooseを利用することでスキーマを定義することができます。またアクセスにはモデルを介して行い、データを保存する際にバリデーションを行うことも可能です。Node.jsからMongoDBに直接アクセスするためのドライバは存在するのでMongooseは必須ではありませんが、今回はMongooseを利用してMongoDBの操作を行います。

本書の中でスキーマの作成やモデルを介してMongoDBにアクセスを行うため都度それらの説明を行なっていきます。スキーマやモデルの意味がこの時点でわからなくても問題ありません。スキーマについては例えばユーザ情報を含むコレクションを作成したい場合、ユーザ名、パスワード、メールアドレスで構成されているよと定義することです。

mongooseのインストールはnpmコマンドで行います。


$ npm install mongoose

RESTful APIの設定

GET, POSTなどのHTTPリクエストを通して外部からnode.jsにアクセスしてデータベースへのデータ追加、更新、削除、取得を行います。

データベースの作成

HomebrewでMongoDBのインストール後、起動を行なっていない場合は下記のコマンドでMongoDBを起動してください。


$ brew services start mongodb-community

起動後データベースの作成を行います。mongoコマンドを実行してMongoDBに接続してください。

MongoDB内に存在するデータベースはshow dbsコマンドで確認することができます。


> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

use “データベース名”を実行するとデータベース名が存在する場合はそのデータベースに接続し、存在しない場合は新しくデータベースを作成します。


> use test_db
switched to db test_db

後ほどGETリクエストの動作確認を行うため、ドキュメントを1つ追加します。


> db.users.insertOne({ name: "John Doe", age: 30})
{
	"acknowledged" : true,
	"insertedId" : ObjectId("5d959442b964357d3df876e6")
}

MongoDBではデータベースの中にコレクション、コレクションの中にドキュメントという単位でデータが保存されますが、コレクションを明示的に作成しなくてもdb.users.insertOneコマンドのusersを指定しているのでこれでコレクションが自動で作成されその中にドキュメントが作成されます。

データベースへの接続とイベント

データベースが作成できたのでデータベースへの接続を行います。app.jsファイルにデータベースの接続に関する設定を行います。


const express = require('express');
const app = express();
const mongoose = require('mongoose');

const port = 3000;

mongoose.connect('mongodb://127.0.0.1/test_db');

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'DB connection error:'));
db.once('open', () => console.log('DB connection successful'));

mongooseを利用してMongoDBへの接続を行うので、requireでmongooseを読み込んでいます。

mongoose.connectでデータベースへの接続を行なっています。127.0.0.1はローカルホストのIPアドレスでtest_dbは先ほど作成したデータベース名を指定します。

db.on, db.onceではデータベースとの接続で発生するイベントを監視しています。onはerrorイベントを監視するために設定し、once(‘open’,..)では接続が完了すると発生するopenイベントを監視し、一度だけ検知するためonではなくonceは使用しています。onでは何度でもイベントを検知しますが、onceだと一度検知するとその後は検知しません。

イベントにはerror, open以外にも下記のものがあります。openやerrorのようにonを設定するとイベントを検知できます。

  • connecting
  • connected
  • disconnecting
  • disconnected
  • close
  • reconnected
  • error
  • fullsetup
  • all
  • reconnectFailed

更新したapp.jsファイルを保存すると下記のメッセージが表示されます。

npx nodemon app.jsが実行されている状態だとapp.jsを更新すると下記のメッセージがnpxコマンドを実行した端末上で家訓人できます。

[nodemon] starting `node app.js`
(node:50660) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:50660) DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.
Example app listening on port 3000!
Database connection successful

useNewUrlParser, useUnifiedTopologyをtrueに設定するようにメッセージが表示されているので、下記のようにoptions設定を行います。


const express = require('express');
const app = express();
const mongoose = require('mongoose');

const port = 3000;

const options = {
	useUnifiedTopology : true,
	useNewUrlParser : true
}

mongoose.connect('mongodb://127.0.0.1/test_db',options);

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'DB connection error:'));
db.once('open', () => console.log('DB connection successful'));

再度更新すると先ほどのメッセージは消え、Database connection successfulを確認することができます。これはdb.onceでopenイベントを検知したため表示されるメッセージです。


[nodemon] restarting due to changes...
[nodemon] starting `node app.js`
Example app listening on port 3000!
Database connection successful

意図的に接続のIPアドレスを127.0.0.1から127.0.0.に変更するとerrorを検知し下記のメッセージが表示されます。on、once設定が正常に動作していることが確認できます。


connection error: Error: getaddrinfo ENOTFOUND 127.0.0.
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26) {
  name: 'MongoNetworkError',
  errorLabels: [ 'TransientTransactionError' ],
  [Symbol(mongoErrorContextSymbol)]: {}
}

データベースの接続と接続に関するイベントの設定が理解できたかと思います。

スキーマの作成、モデル設定

スキーマの作成を行うためにインストールディレクトリ(express_mangodb)の下にmodelsディレクトリを作成し、User.jsファイルを作成します。

スキーマでは、ドキュメントがどのような名前、スキーマタイプで構成されているかを定義することができます。


const mongoose = require('mongoose');

const UserSchema = mongoose.Schema({
  name: String,
  age: Number
});

module.exports = mongoose.model('User',UserSchema);

UserSchemaとしてname, ageから構成され、スキーマタイプをそれぞれStringとNumberに設定しています。

最後の行で、mongoose.modelでスキーマとモデルを紐づけています。Userというモデルを通して、MongoDBにアクセスが可能となります。

作成したUser.jsはapp.jsで読み込みます。


const express = require('express');
const app = express();
const mongoose = require('mongoose');

const User = require('./models/User'); //追加

const port = 3000;

ルーティングの追加

データベース作成時に1件ドキュメントを追加しているので、そのドキュメントが取得できるか確認してみましょう。そのためにはルーティングを追加する必要があります。

app.jsファイルに以下を追加します。ここでは先ほど追加したUserモデルを使っています。findでusersコレクションに入ったすべてのデータを取得していますが、1件しかドキュメントは保存されていないので1件分のデータを取得することができます。


app.get('/user', async (req, res) => {

	const users = await User.find({});

	res.json(users);

});

VSCにREST clientインストールとGETリクエスト

HTTPリクエストを使って動作確認を行うためにVisual Studio Codeの拡張機能REST clientをインストールします。

Visual Studio Codeを使用していない人はPostmanやその他のツールを使ってください。REST Clientはインストールすればすぐに使える大変便利なツールです。
REST clientのインストール
REST clientのインストール

インストールしたら拡張子はrestかhttpで任意のファイルを作成してください。ここではtest.httpという名前でファイルを作成しています。

GET http://127.0.0.1:3000/userとtest.httpファイルに記述すると上部にSend Requestという文字が表示されるので、それをクリックしてください。

GETリクエストを記述
GETリクエストを記述

GETリクエストの結果が右側に表示されます。Expressサーバからの戻り値で、中身を見るとユーザのデータが取得できていることが確認できます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 63
ETag: W/"3f-ES4GpS39OSPAdNJ9H7JM0FnNpjs"
Date: Thu, 03 Oct 2019 07:28:29 GMT
Connection: close

[
  {
    "_id": "5d959442b964357d3df876e6",
    "name": "John Doe",
    "age": 30
  }
]

クライアントからGETリクエストをExpressサーバのルーティング/userに向けて送信し、サーバ側でリクエストを受信後、mongooseのUserモデルを介してtest_dbのusersコレクションからドキュメントを取得し、responseでクライアントにドキュメント(user)を返しています。

ここまでの動作確認で、GETリクエストが正常に動作することが確認できました。

ルーティングファイルを別ファイルに

ここまではルーティング/userしないため、app.jsに記述していますが、DELETEリクエストやPATCHリクエスト用のルーティングの追加が必要となるため、ルーティング部分は別ファイルを作成します。

効率的にメンテナンスを行うため、コードはモジュール化するようにしましょう。

modelsと同様にインストールディレクトリ(express_mongodb)の下にroutesディレクトリを作成、user.jsファイルを作成します。このファイルの中に/userに関するルーティングを追加していきます。

別のコレクションを追加した場合は、user.jsとは別に新たなルーティングファイルを作成します。

作成後、user.jsファイルにapp.jsにあったルーティングを移動して下記のように記述します。


const express = require('express');
const router = express.Router();
const User = require('../models/User');

router.get('/', async (req, res) => {

	const users = await User.find({});

	res.json(users);

});

module.exports = router;
先ほどはget(‘/user’..)でしたが、get(‘/’,…)に変更しています。モデルファイルはapp.jsファイルで読み込みしていましたが、このファイルで読み込みを行うように変更します。

user.jsファイル作成後、app.jsも変更が必要になります。新たにルーティングファイルuser.jsを読み込んでミドルウェアの追加を行います。/userにアクセスがあった場合は、user.jsファイルに記述したルーティグにアクセスすることになります。


const express = require('express');
const app = express();
const mongoose = require('mongoose');
const userRouter = require('./routes/user'); //追加

app.use('/user', userRouter); //追加

const port = 3000;

const options = {
	useUnifiedTopology : true,
	useNewUrlParser : true
}

mongoose.connect('mongodb://127.0.0.1/test_db',options);

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => console.log('Database connection successful'));

app.listen(port, 
	() => console.log(`Example app listening on port ${port}!`));

更新後、REST Clientを使って/userへGETリクエストを送ります。ルーティングファイルを分ける前と同じ結果が取得できることを確認してください。

POSTリクエストでデータ追加

GETリクエストで情報が取得できることを確認できたので、今後はPOSTリクエストでデータの追加を行います。

POSTリクエストをサーバ側で処理するためには、/routes/user.jsファイルに新たにルーティングを追加する必要がります。


router.post('/', async (req,res)=>{
	const user = new User({
		name: req.body.name,
		age: req.body.age
	});

	const savedUser = await user.save();
	res.json(savedUser);

});

クライアント側からjson形式でPOSTリクエストが送られてくるため、送られてるデータを取得するためexpress.json()を追加します。


router.use(express.json());
クライアントからのRequestデータはreq.bodyの中に入ります。もしexpress.json()の設定がなければreq.bodyは何もない状態となり処理することができません。

ルーティングの追加が完了したので、REST ClientからPOSTリクエストを送信します。

先ほどのtest.httpファイルでGETリクエストを記述しましたが、その下にPOSTリクエストを追加することができます。新たにリクエストを追加するときは###を入れることで別リエクストとして追加可能です。


GET http://127.0.0.1:3000/user

###

POST http://127.0.0.1:3000/user
Content-Type: application/json

{
    "name": "John",
    "age": 23
}
POSTリクエスト追加
POSTリクエスト追加

Send Requestで実行するとデータが追加されたことが確認できます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 65
ETag: W/"41-yj6g+gY22xzYTTZe0HWWXk3eR1Y"
Date: Thu, 03 Oct 2019 08:25:54 GMT
Connection: close

{
  "_id": "5d95b09266e5e3c791c631f6",
  "name": "John",
  "age": 23,
  "__v": 0
}

個別データの取得(GETリクエスト)

GETリクエストでusersコレクション全体は取得できましたが、個別のドキュメントを取得する方法を確認しておきます。ここでは、個別のドキュメントを取得する場合は、データ追加時にMongoDB側で自動で付与される_idを利用します。

_idはPOSTリクエストでデータを追加した際の戻り値として取得した_idを利用します。ここでは_idは5d95b09266e5e3c791c631f6です。GETリクエストを送る場合は、GET  http://127.0.0.1:3000/user/5d95b09266e5e3c791c631f6の形でリクエストを送ります。

新たにルーティングを追加しますがGETリクエストの/userの後ろの文字列は、ルーティングに:userIDを設定することで取得することができます。その値は、req.parames.userIDで取得することができます。_idを使ってデータを取得するので、findByIdメソッドを利用します。


router.get('/:userID',(req, res)=>{
	User.findById(req.params.userID,(err,user)=>{
		if (err) console.log('error');
		res.send(user);
	});
});

ルーティングの追加が完了したら、REST Clientのtest.httpファイルに以下のGETリクエストを記述し、Send Requestを送ります。


GET http://127.0.0.1:3000/user/5d95b09266e5e3c791c631f6

GETリクエストが正常に行われると先ほど追加したデータのみ取得することができます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 65
ETag: W/"41-yj6g+gY22xzYTTZe0HWWXk3eR1Y"
Date: Thu, 03 Oct 2019 09:03:19 GMT
Connection: close

{
  "_id": "5d95b09266e5e3c791c631f6",
  "name": "John",
  "age": 23,
  "__v": 0
}

DELETEリクエストでデータ削除

次はDELETEリクエストで追加したデータを削除します。DELETEリクエストも_idを利用して削除するドキュメントを識別して削除を行います。

/routes/user.jsファイルに新たにDELETEリクエスト用のルーティングを追加します。deleteメソッドを使い、削除に利用するidは:userIDを設定し、req.parames.userIDで取得します。削除はremoveメソッドを使います。


router.delete('/:userID',async (req,res)=>{
	const user = await User.remove({_id: req.params.userID});
	res.send(user);
});

ルーティング追加後、REST ClientでDELETEメソッドを記述し、Send Requestボタンを押すと削除が行われます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 31
ETag: W/"1f-W+nAuB2WoFfXCJf017kGogymC54"
Date: Thu, 03 Oct 2019 09:10:24 GMT
Connection: close

{
  "n": 1,
  "ok": 1,
  "deletedCount": 1
}

nとdeletedCountは削除したドキュメントの数で、エラーがない場合はokは1となります。削除が成功したから1というわけではありません。

PATCHリクエストによるデータ更新

再度にPATCHリクエストによるデータの更新を行います。更新は、更新データと更新したいドキュメントを識別する_idを使います。

新たにPATCH用のルーティングを/routes/user.jsファイルに追加します。

_idを使って更新するドキュメントを見つけ、$setプロパティの値に更新したい値を記述します。


router.patch('/:userID',async (req,res)=>{
	console.log(req.body.age);
	const user = await User.updateOne({_id: req.params.userID},{$set:{age:req.body.age}});
	res.send(user);
});

更新は最初に手動で作成したドキュメントの更新を行います。_idがわからない場合はREST Clientを使って/userにGETリクエストを送信して_idを確認してください。ここでは_idは5d959442b964357d3df876e6です。

REST Clientのtest.httpファイルのPATCHリクエストを記述します。


PATCH http://127.0.0.1:3000/user/5d959442b964357d3df876e6
Content-Type: application/json

{
    "age": 88
}

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 28
ETag: W/"1c-9CSp0YZbwVi1S/g+0CyT4pOh1+k"
Date: Thu, 03 Oct 2019 09:19:03 GMT
Connection: close

{
  "n": 1,
  "nModified": 1,
  "ok": 1
}

nModifiedは更新した数でnはマッチした数なので指定した_idを持つドキュメントの数で、エラーでない場合はokは1が戻されます。

再度_idを利用して、GETリクエストで個別データを取得するとageが88になっていることが確認できれば、更新は正常に完了しています。


{
  "_id": "5d959442b964357d3df876e6",
  "name": "John Doe",
  "age": 88
}