Express.js(node.js)からMongoDBへの接続とRestfull APIの設定
本文書では、Node.jsのExpressからNoSQLデータベースであるMongoDBへの接続方法を確認します。最初にスクラッチからExpressサーバの構築を行い、最終的にはVisual Studio Code(VS Code)の拡張機能(Extensions)であるREST Clientを利用してGET, PATCH, DELETEのHTTPリクエストを送信しデータの作成、更新、削除を行います。
動作確認は、macOS環境で行なっており、Node.jsを事前にインストールしておく必要があります。
MongoDBはクラウド上のサービスでも利用することができます。ローカルではなくクラウドで利用したい場合は以下の記事が参考になります。
MongoDBのインストール
macOSなのでパッケージの管理ツールであるHomebrewを利用してMongoDBのインストールを行います。macOS環境へのMongoDBへのインストール方法については、下記の文書を参考に行なってください。
Expressのインストール
Expressライブラリのインストールを行います。インストールを行うディレクトリは任意の名前をつけてください。ここではexpress_mongodbという名前で作成します。
$ mkdir express_mongodb
$ cd express_mongodb/
Expressで使用するパッケージ管理を行うためnpm initコマンドでpackage.jsonファイルの作成を行います。-yオプションをついて対話モードをスキップします。
$ npm init -y
npmコマンドを実行したexpress_mongodbディレクトリにpackage.jsonファイルが作成されます。
次にnpmコマンドでexpressのインストールを行います。インストールが完了するとpackage.jsonファイルにインストールしたexpressの情報が追加されます。
$ npm install express
Expressの動作確認
Hello Worldの表示
Expressのインストールが完了したので、動作確認を行うためにExpressサーバにアクセスしてきたブラウザに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は正常に動作しています。
nodemonのインストール
app.jsファイルの更新を監視するためにnodemonをインストールします。nodemonによりapp.jsファイルの更新を実行するとnodeコマンドが自動で再実行されるようになります。nodemonは開発のみに利用するため–save-devオプションをつけて実行しています。
% npm install nodemon --save-dev
インストール後、下記のコマンドを実行することでnodemonでapp.jsファイルの更新を監視するため自動でapp.jsファイルの再読み込みが行われます。
$ npx nodemon app.js
Expressサーバの起動が必要な動作確認中は基本的に上記のコマンドを常時実行しておきます。
Mongooseのインストール
MongooseはNode.jsからMongoDBを操作するために利用するObject Data Modeling (ODM) ライブラリです。Node.jsからMongoDBの簡単に操作するため間に入り仲介をします。Node.js→Mongoose→MongoDBとなり、Node.jsからMongoDBへの直接的なアクセスは行いません。
MongoDBではNoSQLなのでMySQL、PostgreSQLのようなリレーショナルデータベースのようにスキーマ(データベーステーブルの構造)が必要ではありませんがMongooseを利用することでスキーマを定義することができます。またモデルを介してアクセスを行い、データを保存する際にバリデーションを行うことも可能です。Node.jsからMongoDBに直接アクセスするために利用できるドライバは存在するためMongooseは必須ではありませんが今回はMongooseを利用してMongoDBの操作を行います。
mongooseのインストールはnpmコマンドで行います。
$ npm install mongoose
RESTful APIの設定
GET, POSTなどのHTTPリクエストを通して外部からExpressサーバにアクセスしてデータベースへのデータ追加、更新、削除、取得を行います。
データベースの作成
HomebrewでMongoDBのインストール後、起動を行なっていない場合は下記のコマンドでMongoDBを起動してください。
$ brew install mongodb-community
$ brew services start mongodb-community
サービスが起動しているかどうかはbrew services listコマンドで確認することができます。
起動後データベースの作成を行います。mongoコマンドを実行してMongoDBに接続してください。
$ mongo
MongoDB shell version v4.4.5
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("06acffa3-fe0c-416e-9985-960b87a1be7e") }
MongoDB server version: 4.4.5
・
・
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を指定しているのでこれでコレクションが自動で作成されその中にドキュメントが作成されます。
データベースの中にデータが作成されているかどうかはdb.users.find()で確認することができます。
データベースへの接続とイベント
データベースが作成できたのでExpressサーバからmongoseを介してデータベースへの接続を行います。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ファイルを保存すると下記のメッセージが表示されます。
[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件ドキュメントを追加しているので、Expressサーバを経由してそのドキュメントが取得できるか確認してみましょう。そのためにはルーティングを追加する必要があります。
app.jsファイルに以下を追加します。ここでは先ほど追加したUserモデルを使っています。findでusersコレクションに入ったすべてのデータを取得していますが、1件しかドキュメントは保存されていないので1件分のデータを取得することができます。
app.get('/user', async (req, res) => {
const users = await User.find({});
res.json(users);
});
ブラウザでlocalhost:3000/userにアクセスするとユーザ情報が表示されることを確認することができます。
VSCodeにREST clientインストールとGETリクエスト
HTTPリクエストを使って動作確認を行うためにVisual Studio Codeの拡張機能REST clientをインストールします。
インストールしたら拡張子はrestかhttpで任意のファイルを作成してください。ここではtest.httpという名前でファイルを作成しています。
GET http://127.0.0.1:3000/userとtest.httpファイルに記述すると上部にSend Requestという文字が表示されるので、それをクリックしてください。
GETリクエストの結果がVisual Studio Codeの画面の右側に表示されます。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ファイルに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;
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リクエストを送ります。ルーティングファイルを分ける前と同じ結果が取得できることを確認してください。
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());
ルーティングの追加が完了したので、REST ClientからPOSTリクエストを送信します。
先ほどのtest.httpファイルでGETリクエストを記述しましたが、その下にPOSTリクエストを追加することができます。新たにリクエストを追加するときは###を入れることで別リエクストとして追加可能です。JSON形式で送信を行うのでContent-Typeにapplication/jsonを設定しています。
GET http://127.0.0.1:3000/user
###
POST http://127.0.0.1:3000/user
Content-Type: application/json
{
"name": "John",
"age": 23
}
送信するのはJSONデータなのでJavaScriptのObjectのように”age”:23の後ろに”,”をいれないように注意してください。
POSTリクエストに上に自動で表示されるSend RequestをクリックするとPOSTリクエストがExpressサーバに送信されデータが追加されたことがメッセージから確認することができます。
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
}
POSTリクエストを実行後に再度GETリクエストを実行すると先ほどは1件のドキュメントのみ表示されていましたが追加したドキュメントを含め2件戻されることが確認できます。
個別データの取得(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を送ります。test.httpにリクエストを追加した時は忘れずに###も追加してください。
###
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
}
エラーステータスの設定方法
もしアクセスしたURLにユーザが存在しない場合はresponseにステータスコード404を設定することもできます。
router.get('/:userID', (req, res) => {
User.findById(req.params.userID, (err, user) => {
if (err) res.status(404).json('ユーザは存在しません');
res.send(user);
});
});
test.httpで存在しないユーザIDを設定してGETリクエストを送ると下記のレスポンスをうけとることができます。1行目に404 Not Foundが表示されていることが確認できます。
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 32
ETag: W/"20-aJbbDugHqC3tDDxk9ZyeliapTzo"
Date: Wed, 30 Jun 2021 02:21:12 GMT
Connection: close
"ユーザは存在しません"
try, catchの設定
ユーザが存在しない時はステータス404のNot Foundで戻せることを確認しました。何か内部的に問題があり処理ができなかった場合はステータス500のInternal Serverエラーを戻すこともできます。その時はtry catchを利用することができます。
router.get('/:userID', (req, res) => {
try {
User.findById(req.params.userID, (err, user) => {
if (err) res.status(404).json('ユーザは存在しません');
res.send(user);
});
} catch (err) {
res.status(500).json({
error: {
name: err.name,
message: err.message,
},
});
}
});
nameとmessageに分けたくない場合はjson({ error: error.toString() })と記述できます。
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ボタンを押すと削除が行われます。
###
DELETE http://127.0.0.1:3000/user/5d95b09266e5e3c791c631f6
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
}
REST Clientを利用してCRUD(Create Read Update Delete)を確認することができました。REST Clientを利用してすべて動作確認ができたのでJavaScriptのフレームワークを利用して作成したアプリケーションでもExpressサーバを経由してmongodb内のデータにアクセスを行うことができます。次はフロントエンド側のアプリケーションを利用してフロントエンド側のアプリケーションからHTTPリクエストを送信して正常に動作するか確認してみてください。