サーバレスでWebアプリを作ろうとするときによく私が使うZeit NowというWebサービスを使ったサーバーサイド開発(簡単なAPIの作り方)のはじめ方について全3記事で説明します。(例によってALIS Boot Campで度々説明が必要になるので参考資料です)
記事の目次はこちらです。
① ZEIT Nowへの登録
② Expressを用いたAPIの立ち上げ(本記事)
③ ZEIT NowとExpressを用いたAPIの立ち上げ
本章では、ExpressというNode.jsのライブラリを用いたAPIの立ち上げ方について説明します。本記事では、Nowを使わない(通常の)やり方について説明します。というのも、Nowを使うか使わないかで、フォルダの構成や関数の組み方が変わってくるためです。Expressというのは、シンプルなWebアプリケーションのフレームワークの一種で、サーバーサイドでAPIを開発する時の最も一般的に用いられるものです。
APIの作り方にはある程度決まりごと(統一的な考え方や制約)がありまして、代表的なものにRESTful APIというものがあります。RESTful APIの細かい決まりごとは、もしご興味あれば調べていただければと思います。
まず、VSCodeのTERMINALで適当なディレクトリを作成しましょう。
mkdir api-sample
cd api-sample
以下のコマンドで空のpackage.jsonファイル(パッケージ管理の設定ファイル)を作成します。色々聞かれますが全部エンターで大丈夫です。
npm init
次にExpressをnpmでインストールします。
npm install express
node_modulesというディレクトリとpackage-lock.jsonというファイルが作成されればOKです。
次に、app.jsというファイル(名前は何でもいいです)をVSCodeで開いて、以下のように書きます。
// モジュールの読み込みとインスタンス化
const express = require("express");
const app = express();
// hello worldという文字列を返すAPIの登録
app.get("/", (req, res) => {
res.send("hello world");
})
// listenメソッドを実行して3000番ポートで待ち受け
const server = app.listen(3000, () => {
console.log("Node.js is listening to PORT:" + server.address().port);
});
上書き保存して下さい。
app.jsの一番上に書いた
const express = require("express");
において、require("express")の返し値は関数になっています。ソースコードを見るとcreateApplicationという名前の関数とわかります。つまり、const expressには関数が代入されます。あくまで、関数を引っ張ってきて別の変数(ここではexpress)に代入しているだけですので、関数を実行しているわけではないことに注意が必要です。
関数を実行するには、2行目で
const app = express();
としているように、()を付けます。こうすることで関数が実行されます。ここでは、実行した関数の返し値をappという別の変数に代入しています。
保存できたら、以下のように実行します。
node app.js
すると、Node.js is listening to PORT:3000というメッセージがコンソールに現れ、コマンドが打てない状態になります。アプリを起動中はこのようにTERMINALからコマンドが打てなくなりますが、Ctrl+C(control+C)を押すことでアプリが終了し、コマンドが打てるようになります。
ブラウザを開いて、
にアクセスして見ましょう。hello worldが表示されればOKです。APIを実際に定義したのは下のコードになってます。
// hello worldという文字列を返すAPIの登録
app.get("/", (req, res) => {
res.send("hello world");
})
app.getの部分が、「GETメゾットの定義」を表しており、"/"が、そのAPIを定義する場所を表しています。"/"というのは一番基準の場所(http://localhost:3000)になってます。たとえば、"/"の代わりに"/hello"として
// hello worldという文字列を返すAPIの登録
app.get("/hello", (req, res) => {
res.send("hello world");
})
と変えてみると、http://localhost:3000/helloに変わります。
次に、app.get()の第二引数である
(req, res) => {}
についてですが、これはreqとresという二つの引数を持つ関数です。コールバック関数というもので、app.get()が実行されるとこのコールバック関数が毎回実行されます。reqはrequestの略で、APIリクエストの際に含まれるパラメータが入ります(後ほど例を示します)。resはresponseの略で、APIを返す際のパラメータを設定することができます。今回の例では、
res.send("hello world");
という行が、"hello world"という文字列を送る役割を果たしています。
app.jsで使ったlistenメソッドの第一引数は、ポート番号(そのアプリと通信するための専用ドアの番号)を指定することができ、今回は3000番ポートで通信しています。
const server = app.listen(3000, () => { console.log("Node.js is listening to PORT:" + server.address().port); });
PCアプリを実行中の間だけ、3000番ポートのドアを開いてアプリとの通信ができます。アプリを落としたら3000番ポートは自動的に閉まり、アクセスしても「このサイトにアクセスできません」と出てきます。
これだけだとつまらないので、もう少し複雑なことをしてみましょう。付け加えるのは、OpenSea(Dappsのゲームのキャラクターやアイテムを売買することができる取引サイト)で公開されているAPIを使い、特定のETHアドレスが所有するNFTアセットを取得するAPIです。今回はメインネットではなくrinkebyというテストネット上のNFTの情報を取得し、表示するAPIを書いてみます。
app.jsを開いて、以下のように修正します。
// Expressモジュールの読み込みとインスタンス化
const express = require("express");
const app = express();
// urlモジュールの読み込み
const { parse } = require("url");
// node-fetchモジュールの読み込み
const fetch = require("node-fetch");
// hello worldという文字列を返すAPIの登録
app.get("/", (req, res) => {
res.send("hello world");
})
// openseaのassetを取得するAPIの登録
app.get("/api/opensea-assets", (req, res) => {
// urlパラメータをパース(構文解析)する
const { query } = parse(req.url, true);
// ownerの取得
const { owner } = query;
// 外部APIのURL
const url = `https://rinkeby-api.opensea.io/api/v1/assets/?owner=${owner}&format=json`
fetch(url)
.then((resultOfFetch) => {
return resultOfFetch.json();
})
.then((jsonData) => {
res.send(jsonData);
})
.catch(() => {
res.status(401).end();
});
})
// listenメソッドを実行して3000番ポートで待ち受け
const server = app.listen(3000, () => {
console.log("Node.js is listening to PORT:" + server.address().port);
});
実行の方法は先ほどと同じです。
node app.js
おっと、エラーが出ました。少し遡ると
Error: Cannot find module 'node-fetch'
というエラーメッセージが出ています。node-fetchが無いですね。このようなメッセージが出た場合には、npm installでパッケージをインストールしましょう。
npm install node-fetch
インストールができたら、再びアプリを実行します。
node app.js
今度はうまくいきましたでしょうか。app.get()の引数に"/api/opensea-assets"を指定しましたので、対応するAPIのURLは以下になります。
http://localhost:3000/api/opensea-assets
表示されるのは{"assets": []}です。取得するETHアドレスを設定していないため、空になっています。以下のようにETHアドレスを渡しましょう。
http://localhost:3000/api/opensea-assets?owner=0x9ba9105d4a1cde23fa2ae25c67dea928b975d729
こうすると、ownerという変数に0x9ba9105d4a1cde23fa2ae25c67dea928b975d729というアドレスを入れてapp.jsのAPIに渡すことができます。URLの後ろに?key=valueの形で付け加えるものを、URLパラメータと呼びます。URLパラメータは(req, res) => {}の関数の呼び出しの際、reqに含まれています。以下のようにしてownerを取り出すことができます。
// urlパラメータをパース(構文解析)する
const { query } = parse(req.url, true);
// ownerの取得
const { owner } = query;
const { owner } = query; が見慣れない方は、
const owner = query.owner;
と同じと考えていただければと思います。queryはObjectで、
query = { owner: 0x9ba9105d4a1cde23fa2ae25c67dea928b975d729}
のような形になっており、JavaScriptではドットで変数を繋げてObject内の値を取得することができます。取得したETHアドレスはurl内に${owner}として埋め込みます。
const url = `https://rinkeby-api.opensea.io/api/v1/assets/?owner=${owner}&format=json`
最後に、OpenSeaのAPIをapp.jsから呼び出します。fetch(url)とすることでAPIの呼び出しが可能です。ここで注意が必要なのが、fetchは非同期で実行されるということです。
今回は例として外部APIから情報を取得してそのままres.send()で返す、一見すると意味のないAPIを作っていますが、実際の開発でもそのようなAPIを作る必要が出てくることがあります。それはオリジン間リソース共有(CORS)制限がかかっている場合です。たとえば、ALISのAPIにはCORS制限がかかっており、フロントエンドから直接APIを叩くことができません。一方、今回ご紹介したNode.jsのようなサーバーサイドのアプリケーションからはAPIを叩くことができます。なので、一度自前のサーバーサイドアプリケーションで外部APIから情報を取得し、自分のフロントエンドアプリケーションに情報を横流しすることでCORS制限を突破できます。ALISのサードパーティWebアプリを作る際に度々必要になるテクニックです。
通常プログラミングを実行するときは上から1行ずつ順番に実行される(一つの処理が終わったら次の行の処理が行われる)という動作を想像すると思いますが、非同期処理を行う関数は、処理が終わる前に次の行の実行が始まります。今回は、fetch(url)によってアセット情報を取得し、その情報をres.send()に渡したいので、fetch(url)の処理を待つ必要があります。そこで使われるのが.then()で、非同期処理を行う関数の後ろに.then()をつけることで、その処理が終わって値が返ってきてから次の処理を行うことが可能になります。そのとき、前の処理の返し値がthen内の関数の引数に渡されます。たとえば、
fetch(url).then((result) => {
})
と書いた場合には、fetch(url)の処理を待ち、取得した結果がresultに渡されます。ここでは、
fetch(url).then((resultOfFetch) => {
return resultOfFetch.json();
})
として、fetch(url)の結果をjson形式に変換しています。これができるのは、fetch(url)で取得した情報にjsonという関数が定義されており、この関数を実行することでjson形式で返すようにプログラミングされているためです。ちなみにresult.json()も非同期処理です。なので、次の.then()に繋げて、
fetch(url)
.then((resultOfFetch) => {
return resultOfFetch.json();
})
.then((jsonData) => {
res.send(jsonData);
})
とすると、resultOfFetch.json()の結果のjson形式データがjsonDataにうまいこと渡されます。最後にres.send(jsonData)を書きますと、値がhttp://localhost:3000/api/opensea-assetsから取得できるようになります。
なお、.catch()では、その前までのthenのチェーン内のどこかでエラーが発生した場合に実行する関数を定義できます。
.catch(() => {
res.status(401).end();
});
ここでは、何らかのエラー(たとえば、OpenSea APIが何らかの理由によりアクセスできなくなっている、など)が発生した場合に401エラーを出すようにしています。
非同期処理は難しいので、よくある間違いを例にもう少し説明します。
例えば、先ほどのコードを下のように"うっかり"書いたとします(非同期処理に慣れていないとよくやります)。
app.get("/api/opensea-assets", (req, res) => { // urlパラメータをパース(構文解析)する const { query } = parse(req.url, true); // ownerの取得 const { owner } = query; // 外部APIのURL const url = `https://rinkeby-api.opensea.io/api/v1/assets/?owner=${owner}&format=json` const resultOfFetch = fetch(url); const jsonData = resultOfFetch.json(); res.send(jsonData); })
一見問題なさそうですが、これを実行すると、
TypeError: resultOfFetch.json is not a function
というエラーメッセージが出ます。一見理解しにくいので、何が起こっているか説明します。
まず、
const resultOfFetch = fetch(url);
ですが、fetch(url)の返し値は
Promise { <pending> }
という非同期処理の状態を示すオブジェクトであり、これがresultOfFetchに代入されます。この時点で欲しい情報であるアセット情報がresultOfFetchに入っていないことがわかるかと思います。
次の行を見て見ましょう。
const jsonData = resultOfFetch.json();
resultOfFetchがオブジェクト(Promise)であることから、オブジェクト内のjsonという名前の変数(キー)を探しに行きます(上で出てきたquery.ownerと同じ処理)。ところがresultOfFetchには幸か不幸かjsonという名前の変数はないので、undefined(未定義)が返ってきます。上で出てきたexpress()のように、()をつけるとそれが関数の場合には実行されますので、resultOfFetch.json()はundefinedであるresultOfFetch.jsonを関数だと思って実行しようとしています。undefinedは関数ではありませんので、
TypeError: resultOfFetch.json is not a function
というエラーが出る、ということになります。
さて、正しく実装できていれば、ブラウザから文字の羅列が吐き出されたかと思います。アドレスが保有しているNFTアセットの情報です。
お疲れ様でした。誰かの理解の助けになれたら幸いです。次の記事ではNowを使って同様のAPIを開発する方法と、その際に気をつける点についてご紹介します。
前の記事:ZEIT Nowへの登録
次の記事:ZEIT NowとExpressを用いたAPIの立ち上げ(執筆中)
クレジット(画像素材):