JavaScript
Node.js
Bluemix

Node.jsでnode-rest-clientを使わずシンプルなAPI利用を実装してみる

「Node.jsでxxxを使わずシンプルなxxxを実装してみる」タイトルが気に入ってしまいました。ちょっと無理があっても使っていたり。

今回もどうか生暖かい目でおつきあいください。

概要

Node.js でサーバーを構築する際にメリットとして、他サーバーにアクセスして様々なAPIを利用できる、ことが挙げられます。クライアント側で動作するJavaScriptだと、セキュリティの観点から他サーバーへのアクセスは限定されますからね。

今回はAPIの中で、私が最もわかりやすいと思う、GitHub の「禅」APIを自分のアプリケーションに組み込んでみましょう。

禅APIとは何か?

GitHub zen はアクセスするたびに禅の言葉をランダムに返してくれるサービスです。簡単でわかりやすいので、APIのテストにはお勧め。

とりあえずブラウザで https://api.github.com/zen にアクセスしてみてください。

image.png

禅の言葉が表示され、リロードするたびにランダムに変わっていくのがわかるでしょう。

自分のWebページに、この禅のランダムな言葉を表示させることを考えます。ブラウザで動作するJavaScriptは別サーバーにあるこのAPIにアクセスすることは難しいため、それを中継するサービスを自分のサーバーに配置する必要があります。

今回はアプリケーションに対し /api/zen とアクセスすると上記サービスへ中継する、つまり同じ結果を返すように実装してみます。

普通だとどう書くか?

Express と node-rest-client が使える環境だと、例えば次のように簡単にAPIを追加できます。

var Client = require('node-rest-client').Client;
var client = new Client();

app.get('/api/zen', function (req, res) {
  client.get("https://api.github.com/zen", {headers: {"User-Agent": "simple-nodejs"}}, (data, response) => {
    res.send(data ? data.toString('utf8') : "ERROR");
  });
});

利用の際には User-Agent の指定が必須で、指定が無いと403エラーを返してくるようです。そこだけ注意が必要ですね。

アプリにコードを追加する

さて、モジュールを使わないで、でもなるべくシンプルに同じ機能を実装してみましょう。

組み込む場所

まずは現在のアプリケーションのメインロジックをみてみます。

var server = http.createServer(function (req, res) {
  var url = "public" + (req.url.endsWith("/") ? req.url + "index.html" : req.url);
  if (url.startsWith(basic_realm)) {
    // ここにBASIC認証の処理
    }
  }
  if (!res.finished) {
    if (fs.existsSync(url)) {
      // ここに通常のWebサーバーの処理
    } else {
      // ここにファイルが見つからない404エラー処理
    }
  }
});

今回の api/zen は通常のWebサーバーの処理、に追加しましょう。以下のように場合分けします。

var server = http.createServer(function (req, res) {
  var url = "public" + (req.url.endsWith("/") ? req.url + "index.html" : req.url);
  if (url.startsWith(basic_realm)) {
    // ここにBASIC認証の処理
    }
  }
  if (!res.finished) {
    if (url == "public/api/zen") {
      // ここに /api/zen の処理を追加
    } else if (fs.existsSync(url)) {
      // ここに通常のWebサーバーの処理
    } else {
      // ここにファイルが見つからない404エラー処理
    }
  }
});

実際にファイルが存在するかどうか確認する前に、/api/zen へのアクセスであるか確認し、そうであれば専用の処理に分岐する、というわけです。

便利関数を追加

アプリケーション全体を通じて、ブラウザにコンテンツを返す部分、そしてブラウザにエラーを返す部分、が増えてきました。同じような処理が幾つもあるので、これらの処理をまとめた便利関数を2つ追加しておきます。

function writeContent(_res, _data, _type) {
  _type = _type ? _type : "text/plain";
  _res.writeHead(200, {"Content-Type": _type});
  _res.end(_data);
}
function writeError(_res, _code) {
  console.log("ERROR: " + _code); // for debug
  _res.statusCode = _code;
  _res.end();
}

それぞれ、前回までに良く出てきた処理を単にまとめたものです。テキストデータを返すことが多いことから、_type の値は省略したら "text/plain" になる、あたりがちょっと気を使った所かもしれません。

実際のAPI処理

さて上記の便利関数を使って、禅APIへのアクセスを実装してみましょう。

まず標準モジュールを読み込んでおき、

var https = require('https');

「// ここに /api/zen の処理を追加」の場所には以下のコードを追加します。

var opt = {hostname:'api.github.com', port:443, path:'/zen', headers:{'user-agent':'simple-nodejs'}, method:'GET'};
https.get(opt, (r) => {
  var d = "";
  if (r.statusCode == 200) {
    r.on('data', (_chunk) => d += _chunk);
    r.on('end', () => writeContent(res, d));
  } else {
    writeContent(res, "ERROR: " + r.statusCode);
  }
});

node-rest-client なら1行で書けた処理が、Node.js の標準モジュール https では少し面倒になることがわかります。

実は https.get でも第一引数で url を文字列として直接渡せるのですが、それだとオプションを指定することができません。今回の禅APIは利用の際、UserAgentヘッダが必須で、指定しないと403エラーが返ってきます。

というわけで https.get でヘッダを指定するために、第一引数にはオブジェクト形式を使用し、hostname や port など分けて指定しなければならないようです。

更に https.get では、得られたデータが分割して戻ってきますので、'data'イベントで得られたデータを自分で結合しておき、'end'イベントを待ってそのデータを利用する必要があります。

まあ実は今回の禅APIが返してくるデータは短いテキストなので、分割はされず、実際にはそれぞれ1回ずつしか呼ばれないんですけどね…

なおエラーが発生した場合にはテキストでエラーコードを返す、という仕様にしています。

さて試してみよう!

やっとお試しの時間です!例によってコンソールから node server.js で起動し、ブラウザで http://localhost:3000/api/zen にアクセスしてみましょう。

image.png

ちゃんと禅の言葉が表示されました。リロードしたら言葉は変わりました。やりましたね!

アプリケーションの全ソース

今回は便利関数を追加したので、全体的に少しコードが短くなりました。BASIC認証の機能があり、禅APIをサポートしたWebサーバーアプリケーションですが、全部で91行、サイズは621バイトしかありません。

そろそろ苦しくはなってきましたが、server.js の全ソースコードを掲載しますので、ざっと見てみてください。

var http = require("http");
var https = require('https');
var fs = require('fs');

function getType(_url) {
  var types = {
    ".html": "text/html",
    ".css": "text/css",
    ".js": "text/javascript",
    ".png": "image/png",
    ".gif": "image/gif",
    ".svg": "svg+xml"
  }
  for (var key in types) {
    if (_url.endsWith(key)) {
      return types[key];
    }
  }
  return "text/plain";
}

var basic_realm = "public/member/";
var basic_name = "simple-nodejs_member";
var basic_users = ["QWxhZGRpbjpvcGVuIHNlc2FtZQ=="];
function isUser(_auth) {
  for (var l=0; l<basic_users.length; l++) {
    if (basic_users[l] == _auth) {
      return true;
    }
  }
  return false;
}
function addUser(_id, _pw) {
  var auth = (new Buffer((_id + ':' + _pw).toString(), 'binary')).toString('base64');
  if (!isUser(auth)) {
    basic_users.push(auth);
  }
}
addUser("yamachan", "123");

function writeContent(_res, _data, _type) {
  _type = _type ? _type : "text/plain";
  _res.writeHead(200, {"Content-Type": _type});
  _res.end(_data);
}
function writeError(_res, _code) {
  console.log("ERROR: " + _code); // for debug
  _res.statusCode = _code;
  _res.end();
}

var server = http.createServer(function (req, res) {
  var url = "public" + (req.url.endsWith("/") ? req.url + "index.html" : req.url);
  console.log(url); // for debug
  if (url.startsWith(basic_realm)) {
    var auth = req.headers["authorization"]||"";
    if (!auth.startsWith("Basic ") || !isUser(auth.substring(6))) {
      res.setHeader('WWW-Authenticate', 'Basic realm="' + basic_name + '"');
      writeError(res, 401);
    }
  }
  if (!res.finished) {
    if (url == "public/api/zen") {
      var opt = {hostname:'api.github.com', port:443, path:'/zen', headers:{'user-agent':'simple-nodejs'}, method:'GET'};
      https.get(opt, (r) => {
        var d = "";
        if (r.statusCode == 200) {
          r.on('data', (_chunk) => d += _chunk);
          r.on('end', () => writeContent(res, d));
        } else {
          writeContent(res, "ERROR: " + r.statusCode);
        }
      });
    } else if (fs.existsSync(url)) {
      fs.readFile(url, (err, data) => {
        if (!err) {
          writeContent(res, data, getType(url));
        } else {
          writeError(res, 500);
        }
      });
    } else {
      writeError(res, 404);
    }
  }
});

var port = process.env.PORT || 3000;
server.listen(port, function() {
  console.log("To view your app, open this link in your browser: http://localhost:" + port);
});

APIを利用する

さて、せっかくアプリに禅APIを組み込んだのですから、使ってみましょう。

public/indsex.html に配置してあるトップページのボディに以下のhtmlを追加します。

    <button onclick="getZen()">Get zen word</button>
    <p id="zen_area" style="border:solid 1px #ccc; width:20em;"></p>

アプリを再起動する必要はありません。ブラウザでトップページにアクセスしてみましょう。

image.png

ボタンと表示エリアが追加されました。

更に index.html の <head> エリアに以下のJavaScriptコードを記載します。クライアント側なので今回は jQuery を使っています。

 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
  function getZen() {
    $.get("/api/zen", function(data){
      $("#zen_area").append(data + '<br />');
    });
  }
</script>

Google CDN 様、いつもありがとうございます。感謝しています!

まあ簡単な jQuery アプリですが、ボタンが押されたら /api/zen の禅APIにアクセスし、得られた結果を表示エリアに追記、という動作をします。

さあ、ブラウザをリロードして、何度かボタンを押してみましょう。

image.png

ボタンを押すたび、禅の言葉が下に追記されていきます。

これはすごく簡単なサンプルなのですが、別サーバーにある禅APIを利用しているので、単なるWebページ、つまりはクライアント側のJavaScriptだけでは実装が難しいものです。

Node.jsで作成した中継サービスがあったからできた、独自サーバーだからできた、と自己満足しても許される状況です。自分すごい。褒めてあげましょう!

最後に public/index.html の内容も全部載せておきます。

<!doctype html>
<html lang="ja">
  <head>
    <title>simple-nodejs top</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script>
      function getZen() {
        $.get("/api/zen", function(data){
          $("#zen_area").append(data + '<br />');
        });
      }
    </script>
  </head>
  <body>
    <img src="i/yamachan.png"/>
    <p>Hello, <b>Node.js</b> world!</p>
    <button onclick="getZen()">Get zen word</button>
    <p id="zen_area" style="border:solid 1px #ccc; width:20em;"></p>
  </body>
</html>

過去シリーズ

実はこの投稿、同じサンプルをだんだん拡張しながら投稿しています。以下の順で拡張しているので、わからない点があれば過去の投稿も参考にしていただければ嬉しいです。

単に読むだけであれば今回の投稿だけでokですが、ソースファイルを自分でも実行してみたい方は最後の(一番下の)投稿の ダウンロード からzipファイルをダウンロードして、それを拡張していってください。

Bluemix環境にpushしてみる (おまけ)

今回も例によって cf push で Bluemix 環境に上げました。ローカルと同様に禅APIが利用できることが確認できます。

image.png

このあたりの詳細は最初の投稿、 IBM Bluemixで最もシンプルなNode.js環境を作成してみる を参考にしてみてください。

この記事を公開直後にはまだサービスが動いているとおもいますので、興味のある方はアクセスしてみてください(笑)

ダウンロード

今回のプロジェクトの全6ファイルを zipにまとめました ので、必要でしたらダウンロードしてお使いください!

ライセンス

この投稿に含まれる私の作成した全てのコードは Creative Commons Zero ライセンスとします。自由にお使いください。

Enjoy!

以上、シンプル…かどうかは謎ですが、Node.jsの基本機能で外部サーバーにアクセスし、アプリケーションにAPIサービス(単なる中継だけど)を追加できました。

これを応用すると、ネット上の様々なAPIを活用して、自分のサイトに生かすことができそうです。その小さな第一歩として、この投稿が何らかの参考になれば嬉しいです。いろいろ楽しいことやりたいですよね。

ではまた!