Node.js + Express + あえてCloudant で REST APIを作成する（開発編②）


## はじめに
[Node.js + Express + あえてCloudant で REST APIを作成する（開発編①）](qiita.com/rinaxsumomo/items/a2e72a432a3404177775)の続きです。
CRUDを行うサンプルコードを開発してローカル環境とBluemix環境で実行します。

## CRUDのサンプルコード
* メイン・スクリプトを編集：
	* ここでcloudantClient.jsを読み込み、/animalsとパスを切って, cloudantClient.jsを実行させます。
	* パスの指定方法がREST API設計では重要です。

```app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
// 今回追加部分
var cloudantClient = require('./routes/cloudantClient');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'e');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
// 今回追加部分
app.use('/animals',cloudantClient);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

```


* 今回開発したコード全体：

```cloudantClient.js

require('dotenv').load();

var express = require('express');
var router = express.Router();

// Cloudantの認証とサンプルのanimaldbの指定
var Cloudant = require('cloudant');
var me = process.env.cloudant_username; // Set this in .env file
var password = process.env.cloudant_password; // Set this in .env file
var cloudant = Cloudant({account:me, password:password}); 
var db = cloudant.db.use("animaldb");

// GETでドキュメント参照とエラーハンドリング
router.get('/:_id', function(req, res, next){
	db.get(req.params._id, function(err, data){
		if(!err){
			res.set('X-RateLimit-Limit','180');
			res.status(200).json({"document":data});
			//res.status(200).send(JSON.stringify(data));
			console.log(data);
		}
		else if(err.statusCode == 404){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(404).json({"error":{"code":404,"message":"Resource not found, Specify the correct id","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else if(err.statusCode == 500){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(500).json({"error":{"code":500,"message":"Internal Server Error,Please retry later","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else{
			console.log("Error:", err);
			res.set('X-RateLimit-Limit','180');
			res.status(err.statusCode).json({"error":{"code":err.statusCode,"message":"Other error","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
	});
});

//POSTでドキュメント作成とエラーハンドリング
router.post('/', function(req, res, next) {
  db.insert(req.body, req.body._id, function(err, data){
		if(!err){
			var id = req.body._id;
			var url = './animals/'+ id;
			res.set('X-RateLimit-Limit','180');
			res.status(201).json({"created":data,"url":url});
	    //res.status(201).send(JSON.stringify(data));
			console.log(data);
		}
		else if(err.statusCode == 409){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(409).json({"error":{"code":409, "message":"Resource already exists","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else if(err.statusCode == 500){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(500).json({"error":{"code":500,"message":"Internal Server Error,Please retry later","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else{
			console.log("Error:", err);
			res.set('X-RateLimit-Limit','180');
			res.status(err.statusCode).json({"error":{"code":err.statusCode,"message":"Other error","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
 });
});

//PUTでドキュメント更新とエラーハンドリング
router.put('/:_id', function(req, res, next){
  db.insert(req.body, function(err, data){
		if(!err){
			res.set('X-RateLimit-Limit','180');
			res.status(200).json({"updated":data});
			//res.status(200).send(JSON.stringify(data));
			console.log(data);
		}
		else if(err.statusCode == 409){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(409).json({"error":{"code":409,"message":"Update failed, specify the correct id and rev","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else if(err.statusCode == 500){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(500).json({"error":{"code":500,"message":"Internal Server Error,Please retry later","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else{
			console.log("Error:", err);
			res.set('X-RateLimit-Limit','180');
			res.status(err.statusCode).json({"error":{"code":err.statusCode,"message":"Other error","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
  });
});

// DELETEでドキュメント削除とエラーハンドリング
router.delete('/:_id/:_rev', function(req, res, next){
  db.destroy(req.params._id,req.params._rev, function(err, data){
		if(!err){
			res.set('X-RateLimit-Limit','180');
			res.json({"deleted":data});
			res.status(204);
	    console.log(data);
		}
		else if(err.statusCode == 404){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(404).json({"error":{"code":404,"message":"Resource not found, Specify the correct id and rev","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else if(err.statusCode == 409){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(409).json({"error":{"code":409,"message":"Delete failed, specify the correct id and rev","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else if(err.statusCode == 500){
			console.log("Error:", err.statusCode);
			res.set('X-RateLimit-Limit','180');
			res.status(500).json({"error":{"code":500,"message":"Internal Server Error,Please retry later","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
		else{
			console.log("Error:", err);
			res.set('X-RateLimit-Limit','180');
			res.status(err.statusCode).json({"error":{"code":err.statusCode,"message":"Other error","info":"https://docs.cloudant.com/http.html#http-status-codes"}});
		}
  });
});

module.exports = router;

```


## 動かしてみる
以下のコマンドでapp.jsを実行します。

```
$ npm start
```
![Kobito.blvkgP.png](https://qiita-image-store.s3.amazonaws.com/0/129662/b166b902-d7ea-000f-3b85-799685504a04.png "Kobito.blvkgP.png")


### Chrome Postmanからアクセス:作成
* URLは/animals以降は指定せず、ボディに以下のJSONを入れ、application/Jsonを指定し、POSTリクエスト送信＆結果：
![Kobito.kw02he.png](https://qiita-image-store.s3.amazonaws.com/0/129662/d031072a-d57b-a66a-068b-d26033b8eacd.png "Kobito.kw02he.png")

### Chrome Postmanからアクセス:参照
* URLは/animals/catを指定し、上記で作成したCatドキュメントにGETリクエスト送信＆結果：
![Kobito.37Znnj.png](https://qiita-image-store.s3.amazonaws.com/0/129662/b781cab8-8560-b8e7-ba89-365d1d7d7a0a.png "Kobito.37Znnj.png")

### Chrome Postmanからアクセス:更新
* URLは/animals/catを指定し、ボディに以下のJSONを入れ、application/Jsonを指定し、PUTリクエスト送信＆結果：
* POSTで作成したときの違いはボディに_revが入っていることです。あとは各項目好きなところを更新します。
* レスポンス・ボディで_rev番号が更新されていることがわかります。
![Kobito.91QozQ.png](https://qiita-image-store.s3.amazonaws.com/0/129662/7228b042-baf3-669c-c926-6c311649226e.png "Kobito.91QozQ.png")

### Chrome Postmanからアクセス：削除
* ボディは指定せず、URLに_idと_revを指定し、DELETEリクエスト送信＆結果：
![Kobito.BREENW.png](https://qiita-image-store.s3.amazonaws.com/0/129662/7cb2c205-cdca-ffc7-e1f0-1ffbd9565ec0.png "Kobito.BREENW.png")


### Bluemixにデプロイしてみる
```cfコマンド
$ cd AppName // アプリのディレクトリに移動
$ cf push AppName // BluemixにCLIでログインしてあるのが前提です
```

Bluemix上で開始されていることが確認できます。
![Kobito.FNxjPE.png](https://qiita-image-store.s3.amazonaws.com/0/129662/75c9ef15-5ec1-fa54-957a-99a52df07c55.png "Kobito.FNxjPE.png")

* http://\<hostname.mybluemix.net>/animals/ に対して、同様にPostmanから各HTTPメソッドを試して問題ないことを確認します。
* Catドキュメントに対するGETリクエスト送信＆結果：
![Kobito.iZEw3n.png](https://qiita-image-store.s3.amazonaws.com/0/129662/f00340ba-23a1-2763-1cac-fff9b9a98d8e.png "Kobito.iZEw3n.png")

## まとめ
以上で(あえて)Cloudant NoSQL DBのサンプルDBに対してCRUD操作のREST APIをNode.jsで実装してみました。
コードは初級レベルなので、よりよい書き方がありましたらコメントいただけますと幸いです。

## つまったところ・注意点など
* node app.js の起動方法はExpress3、npm start の起動方法はExpress4、といったように、お作法がバージョンで結構違いますが、ネット記事などの情報はExpress3ベースがまだ多いので、検索するときは注意が必要です。
* Firefox PosterだとJSONをPOSTするときに意図しない改行コードなど入ることがあり、意図したとおりのデータ挿入ができなかったため、Chrome Postmanで実行したところ成功しました。これからはPostmanを使うつもりです。

## 参考文献
* https://www.npmjs.com/package/cloudant
* https://www.npmjs.com/package/express
* https://github.com/cloudant/nodejs-cloudant/blob/master/example/crud.js
* https://github.com/cloudant-labs/cloudant-nano#document-functions
* http://expressjs.com/
* https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Functions
* https://howtonode.org/bogart-couchdb
* https://console.ng.bluemix.net/docs/services/Cloudant/api/document.html
* https://blog.risingstack.com/10-best-practices-for-writing-node-js-rest-apis/