イッツァハローワールド

恥さらしていこうかなとか。

Node.js(restify) + MongoDB(mongoose) でAPIサーバつくってHerokuでデプロイするまで

先日ようやくリリースしたアプリ「お階段登り」では、ただ階段を上るだけのクソアプリと思いきや、他のユーザーの位置まで来たらすれ違ってみたり、一番上にいる人の段数をとってきたり、影でコソコソやっております。(クソなのには変わりませんが。)
https://itunes.apple.com/jp/app/o-jie-duan-dengri-zuo-rino/id956171342?mt=8&uo=4&at=10l8JW&ct=hatenablog


コソコソやるためにサーバーを自作しました。

サーバー側でやってること例

  • ユーザー登録
  • 自分のスコアを更新する
  • 1位のスコアを取得する
  • 自分が1位かどうか返す
  • 自分のスコアに一番近いユーザの情報を返す(→ユーザのスコアまで到達したらすれ違い実行)

アプリからはHTTPリクエスト投げ、JSON形式でレスを取得してレスによって描画を変えたりします。
こんなAPIサーバー的なものをNode.jsをつかって実装しました。

備忘録でサーバー作成からHerokuへのデプロイまでやり方を残します。

環境

  • Mac OS 10.10.2
  • Node.js v0.10.25
  • MongoDB 2.6.7

事前に勉強したこと

本件の記事と直接関係ないので、別記事にします。
↓こちら

Node.js + MongoDBでAPIサーバつくるまでに勉強したこと - イッツァハローワールド

なんでわざわざRestifyつかったか。

ググったら「REST Webサービスの構築に特化したフレームワーク」って出てきたのでおおそうかつかってみよ!って感じで使いました。
今回つくるような簡単なもんならExpressで十分に思えます。(作った後でわかりましたw)

あと、英語のドキュメントで四苦八苦してみたかったっていうのもちょこっとあります。

今回作るものの仕様

  • ユーザーごとに以下のデータを持つ
    • ユーザーID(id)
    • スコア(score)
  • サーバーはAPICRUDな動きをする
  • レスポンスはJSONで返す

あとは適当に。

まずはRestifyをつかって簡単なサンプルコードをつくる

Node.js入れるところとかは割愛します。

restifyをインストール

$ npm install restify

サンプルコード実装

以下は、http://0.0.0.0:8080/hello_worldにアクセスするとmessage : Hello World!って内容のJSONを返す例。

// sample.js
var restify = require('restify');

// サーバー生成
var server = restify.createServer();

// http://0.0.0.0:8080/hello_worldにGETリクエストしたときの処理
function helloWorld(req, res, next){
	// レスポンス
	res.send({message: 'Hello World!'});
}

// パスと関数の紐付け
// ↓はhttp://0.0.0.0:8080/hello_worldにGETリクエストしたらhelloWorld関数を実行
server.get('/hello_world', helloWorld);

server.listen((8080), function() {
	console.log('%s listening at %s', server.name, server.url);
});

実行

コマンド叩いて実行。

$ node sample.js

http://0.0.0.0:8080/hello_worldを開いてみる

f:id:hanamiju:20150305020831p:plain

できた。こんなかんじで、APIごとに関数と、関数とパスとの紐付けを定義すればわりと簡単に作れます。
以下はmongooseを落として、MongoDBをつかってCRUDします。

実際のAPIサーバ実装やってみる

まずMongoDB, mongoose入れる

$ npm install mongodb
$ npm install mongoose

コード

// sample.js
var restify = require('restify');
var server = restify.createServer();	// サーバー生成
server.use(restify.bodyParser());	// POSTデータをパースするひとを登録
server.use(restify.queryParser());	// クエリをパースするひとを登録

var mongoose = require('mongoose');
var uristring = 'mongodb://localhost/';

var db = mongoose.connect(uristring);
var Schema = mongoose.Schema;

// スキーマの定義
var userSchema = new Schema({
	score: Number,
});

// モデルを生成
mongoose.model('user', userSchema);
var User = mongoose.model('user');

// Create
function createUser(req, res, next){
	var user = new User();	// コレクションを作成
	user.score = 0;
	// つくったコレクションを保存
	user.save(function(err, data){
		res.send({function:'Create', status: 'OK', id: data._id});
	});
}

// Read
function getUsers(req, res, next){
	User.find(function(arr, data){
	// すべてのコレクションの情報を返す
		res.send(data);
	});
}

// Delete
function deleteUser(req, res, next){
	// 入力されたIDのコレクションを削除する
	User.remove({ _id: req.params.id }, function(err){
		res.send({function:'Delete', status: 'OK'})
	});
	return next();
}

function updateScore(req, res, next){
	// IDを検索
	User.findById(req.params.id, function (err, data) {
		// エラー処理
		if (err) {
			res.send({function:'Update', status:'NG', reason: 'User not found.'});
			return next();
		};
		// 入力パラメータ異常
		if (req.params.id == '' || req.params.id === undefined ) {
			res.send({function:'Update', status:'NG', reason: 'User not found.'});
			return next();
		};
		if (req.params.score == '' || req.params.score == 0) {
			res.send({function:'Update', status:'NG', reason: 'Score is invalid.'});
			return next();
		};
		// 入力されたIDのスコアを入力されたものに変更する
		User.update({ _id: req.params.id }, { $set: {score: req.params.score} },
		{ upsert: false, multi: true }, function(err) {
			if (err) {
				res.send({function:'Update', status:'NG', reason: 'Update faild'});
				return next();
			};
			res.send({function:'Update', status:'OK'});
		});
	});
}

server.get('/get_users', getUsers);
server.post('/delete_user', deleteUser);
server.put('/update_score',updateScore);
server.post('/create_user',createUser);

server.listen((8080), function() {
	console.log('%s listening at %s', server.name, server.url);
});

動作確認

実際に各API呼んだらどんなレスポンスを返すか見てみます。
動作を見るためにChromeのエクステンションのAdvanced REST Clientをつかいます。

create(create_user)

まずはコレクションを生成してみます。
Advanced REST Clientを起動し、Reqestタブを開きます。
POSTを選択し、パスを入力します。
f:id:hanamiju:20150305221011p:plain

入力したら「Send」ボタンを押下するとレスポンスが帰ってきます。
f:id:hanamiju:20150305221058p:plain

これでひとつコレクションができました。

get(get_users)

つぎに生成したコレクションを取得します。
こんどはGETを選択します。
f:id:hanamiju:20150305221224p:plain

「Send」ボタン押下で、先ほど作ったコレクションが取得できます。
f:id:hanamiju:20150305221253p:plain

update(update_score)

コレクションのscoreを更新します。
こんどはPUTを選択し、更新するid, 更新後のscoreを入力します。
ためしに1234を入れてみます。
f:id:hanamiju:20150305221640p:plain

「Send」ボタン押下で、更新完了が帰ってきます。
f:id:hanamiju:20150305221724p:plain

再度GETしてスコアを見てみます。
f:id:hanamiju:20150305221758p:plain
更新されました。

入力エラー用のコードも書いてみたので動作を確認してみます。
ためしに、idを存在しない値に書き換えて「Send」ボタンを押下すると
f:id:hanamiju:20150305221852p:plain
エラーが帰ってきました。

delete(delete_user)

つくったコレクションの削除を行います。
POSTを選択して、削除したいコレクションのidを入力します。
f:id:hanamiju:20150305222018p:plain

「Send」ボタン押下でコレクションが削除されます。
f:id:hanamiju:20150305222121p:plain

GETして確かめてみます。
f:id:hanamiju:20150305222233p:plain
DBが空になってました。

やった!

Herokuへデプロイしてみる

ここまでで実装はほぼ終わり。Herokuへデプロイしてみます。

Heroku toolbeld(Heroku推奨)のインストール


Herokuにログイン

$ heroku login

Herokuへ登録した時のメールアドレスとパスワードを聞かれるので入力します。

package.json作成

適当に入力します。

$ npm init

今回作成したのはこんな感じです。

{
  "name": "sample",
  "version": "0.0.1",
  "description": "sample api server",
  "main": "sample.js",
  "dependencies": {
    "mongodb": "~1.4.33",
    "mongoose": "~3.8.24",
    "restify": "~2.8.5"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "hanamiju",
  "license": "MIT"
}

Heroku用のProcfileを作成

web: node sample

こやつにHerokuから呼ぶjsファイルを記載します。拡張子はいらないです。

Herokuのプロジェクト作成

プロジェクトを生成する。今回はapiserverkunnという名前で登録しました。

$ heroku create apiserverkunn

MongoLabをいれる

先ほどまではローカル環境にMongoDBを構築してましたが、デプロイにあたりMongoLabを入れます。
MongoLabはHerokuのアドオンのひとつで、HerokuでMongoDBを496MBまで無料で使えます。最高。
というわけでアドオンを探します。Mongoとか検索ワードで入れればでてきます。
f:id:hanamiju:20150305212133p:plain

こいつのfreeプランをapiserverkunnに適用させます。
上記のようにブラウザからでもいいし、

$ heroku addons:add mongolab

でもオーライ。

sample.jsファイルを修正する。

sample.jsはこのままだとローカル環境でしか動かないので、ファイルを一部修正します。
まずはMongoDBとの接続時のパス

var uristring = 'mongodb://localhost/';  // 変更前
var uristring = process.env.MONGOLAB_URI || 'mongodb://localhost/'; // 変更後

これで、process.env.MONGOLAB_URIに接続し、存在しない場合はmongodb://localhost/に接続される。
つまり、ローカルと本番でコードを改変せずに済むということ。

つぎにサーバー処理開始時

server.listen((8080), function() {    // 変更前
server.listen((process.env.PORT || 8080), function() {    // 変更後

簡単に実行環境毎の設定でアプリケーションを実行することができます。

Herokuへデプロイ

$ git init
$ git add .
$ git commit -m "hoge"
$ git push heroku master

結果を見てみる

あらかじめcreateしといてから、getしてレスポンスを見てみます。
f:id:hanamiju:20150305223334p:plain

ちゃんと表示されました。ばんざい!

まとめ

  • Node.js (restify) つかえばRESTなAPIサーバーが簡単に作れる。
  • Herokuがあれば簡単にデプロイできる
  • MongoLabつかえば、タダで大容量のDB使える。

→ 簡単なCRUDサーバーならタダで簡単に作れる。