Firebase Cloud Functions, Realtime DatabaseでCRUD REST WebAPIを作る

# 前置き
外部からhttpsで接続出来るテスト用のWebAPIをとりあえず作成したいことがありました。固定のjsonを返却するだけのWebAPIを作成すれば良かったのですが、興味本位でRealtime Databaseも使って簡単なCRUD機能を提供するWebAPIを作りました。というよりもちょっと寄道して作ってしまいました:sweat_smile:
「ソースコードだけ見られれば十分」と思われる方々もいらっしゃると思いますが、ひととおり筆者が行った手順も含めて記事にしました。ソースコードは[GitHub](https://github.com/devnokiyo/firebase-cloud-function-rest-api)に公開しています。
※検証した実際のURLを掲載していますが削除する予定です。ご了承ください。

# なぜFirebaseを選んだか
jsonファイルをホスティングすれば良いのですが「せっかくならCRUDくらい付いていると嬉しい」という欲が出てしまいました。そのため、ホスティング案を除外して無料で動的なプログラムが動作する環境として、すぐ浮かんだ3つを検討しました。

1. Heroku
スキルセットの観点からRailsを利用することになりますが、APIモードでプロジェクトを作成するにしても、少し大掛かりな印象がありました。ほんの少しCRUD出来れば満足なので今回は採用を見送りました。

1. AWS
API Gateway+Lambda+DynamoDBを利用することも検討しました。公式のサンプルにCloudFormationのymlもあり構築も楽できそうでした。一番興味はありましたが、API Gatewayの無料枠は12ヵ月の期限付きなので見送りました。他のサービスでも途中で規約が変わることがありますが現状で判断しました。

1. Firebase
モバイル開発でも利用していて上記2つのサービスよりも馴染みがあったことと、自動的に課金されない明示的な無料枠の安心感も手伝って採用することにしました。Cloud FunctionsやRealtime Databaseに興味があるわけではありませんでした・・・:dizzy_face:

消去法で選ばれてしまったFirebaseでありますが、気を取り直して前向きに作業を進めてます:grin::grin:

# 事前準備
1. node.jsをインストール

    ```
$ node -v
v10.15.2
```

1. npmをインストール

    ```bash
$ npm -version
6.8.0
```
1. Firebase CLIをインストール

    ```
npm install -g firebase-tools
```

  1. ログイン
  以下のコマンドを実行します。

        ```
$ firebase login
```
  1. そのままEnterを押下

        ```
    ? Allow Firebase to collect anonymous CLI usage and error reporting information?
 (Y/n) 
```
  1. ブラウザが起動するのでログイン
![01.png](https://qiita-image-store.s3.amazonaws.com/0/240974/a8b795be-b019-0e0a-c202-39a1a7c47f17.png)

  1. 諸々承認
ブラウザでは以下のような表示になっています。
![02.png](https://qiita-image-store.s3.amazonaws.com/0/240974/6f23d6c2-e487-a889-67b9-ad20e8c9aa28.png)
コンソール上では以下のように表示されています。

        ```
    Waiting for authentication...

        ✔  Success! Logged in as devnokiyo@example.com
```

# プロジェクトを作成する
プロジェクトを作成したいパスに移動して以下のコマンドを実行します。
対話式で質疑についても例として記載します。ご参考まで。

```
$ cd repos
$ firebase init functions
? Select a default Firebase project for this directory: [create a new project]
? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
? Do you want to install dependencies with npm now? Yes
```
1. Firebaseコンソールでプロジェクト作成
![03.png](https://qiita-image-store.s3.amazonaws.com/0/240974/48dfe0a9-b054-6886-6ca1-60c086b96e5a.png)
1. Firebase CLIでプロジェクトを追加
対話式で質疑についても例として記載します。ご参考まで。

    ```
$ firebase use --add
? Which project do you want to add? hoge-rest
? What alias do you want to use for this project? (e.g. staging) hoge-rest
```

# Realtime Databaseを準備する
1. Realtime Databaseを作成  
Cloud Firestoreという新しいデータベースも用意されていますが、従来のRealtime Databaseを利用することにします。  
※スクリーンショットを撮直したのでプロジェクト名が異なっています。
![100.png](https://qiita-image-store.s3.amazonaws.com/0/240974/9f9eb79a-7390-584d-8565-2ace6365a60a.png)
1. セキュリティルールを設定  
今回は「ロックモードで開始」を設定します。
![スクリーンショット 2019-03-02 23.53.35.png](https://qiita-image-store.s3.amazonaws.com/0/240974/e1ac4802-9141-7bca-95b7-93e7ecf74944.png)

1. テストデータをインポート
データ取得用のファンクションの動作確認をするためテストデータを用意します。Firebaseコンソールの画像の赤い部分をクリックして「JSONをインポート」を選択します。
![04.png](https://qiita-image-store.s3.amazonaws.com/0/240974/498988d2-7695-8890-8bd0-a1be8dc1dabd.png)
以下をインポートします。

    ```seed.json
    {
      "users": [
        {
          "user_id": "A0001",
          "user_name": "chiyo",
          "age": 9
        },
        {
          "user_id": "A0002",
          "user_name": "eru",
          "age": 5
        },
        {
          "user_id": "A0003",
          "user_name": "otome",
          "age": 13
        }
      ]
    }
    ```
インポートするとデータが可視化されます。
![05.png](https://qiita-image-store.s3.amazonaws.com/0/240974/5448fa05-bfb6-5e8d-50cc-8180020bdc37.png)
1. インデックスを作成
user_idをキーに検索するファンクションを作成するのでインデックスを作成しておきます。FirebaseコンソールのDatabaseでルールを選択します。以下を追加します。

    ```
"users": {
      ".indexOn": ["user_id"]
}
```
Firebaseコンソール上では以下のようになります。
![06.png](https://qiita-image-store.s3.amazonaws.com/0/240974/4175c142-f5fc-5265-28ca-8bb78600d134.png)

# ファンクションを作成してデプロイする
ようやく本題に入ります。ファンクションのソースコードは[GitHub](https://github.com/devnokiyo/firebase-cloud-function-rest-api)に公開しています。宜しければご覧ください。

1. [プロジェクトを作成](https://qiita.com/items/26de016b0baf60b26c90#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)したディレクトリに移動
簡単に構成を確認します。ご察しと思われますがindex.jsにプログラムを書きます。

    ```
    ├── firebase.json
    └── functions
        ├── index.js
        ├── node_modules
        ├── package-lock.json
        └── package.json
    ```

1. Usersを全件取得するファンクションを作成
まずは最低限のnpmパッケージのみを利用して動作させてみます。

    ```js:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');

    admin.initializeApp();
    exports.users = functions.https.onRequest((req, res) => {
      const ref = admin.database().ref('users');
      ref.once("value", function(data) {
        res.send(data);
      });
});
```

1. デプロイ
functionsディレクトリに移動して、デプロイコマンドを実行します。

    ```bash
$ cd functions
$ ls
index.js		package-lock.json
node_modules		package.json
$ firebase deploy --only functions
        :
        :
=== Deploying to 'hoge-rest'...

    i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint
        :
        :
✔  Deploy complete!

    Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/hoge-rest/overview
```
上記のメッセージどおりデプロイに30秒ほど掛かります:hourglass_flowing_sand::hourglass_flowing_sand::hourglass_flowing_sand:
余談ですが、筆者は「修正が反映されていない」と思ったことが何度もありました:sweat:

1. ファンクションを実行
    1. トリガーを確認  
FirebaseコンソールのFunctions ダッシュボードより該当のファンクションのトリガーを確認します。
![07.png](https://qiita-image-store.s3.amazonaws.com/0/240974/523ab464-b9b9-87ca-1b6d-70d0a563e201.png)
    1. ブラウザでURLにアクセス  
jsonでUsersの全件が返却されていることを確認します。
![08.png](https://qiita-image-store.s3.amazonaws.com/0/240974/81ab6af7-d3ee-b3f9-f3e9-39bc00b04c9d.png)

# expressを導入してファンクションを書替える
WebAPIを機能追加するにあたりルーティングを体系的に記述したいのでexpressを導入します。

1. npmパッケージをインストール  
functionsディレクトリに移動してnpmパッケージをインストールします。

    ```bash
$ npm install express --save
$ npm install body-parser --save
```
1. ファンクションをexpress版に書替え
express版に書替えます。筆者としてはもう一つやりたいことがあります。最低限のnpmパッケージのみを利用して動作させたファンクションでは複数のUserを連想配列を返していましたが、クライアント側で扱い易さを考慮して通常の配列を返すように変更します。

    ```javascript:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require('express');
const bodyParser = require('body-parser');

    const app = express();
app.use(bodyParser.json());

    admin.initializeApp();

    app.get('/', (req, res) => {
      let users = []
      const query = admin.database().ref("users").orderByKey();
      query.once("value").then(snapshot => {
        snapshot.forEach(childSnapshot => {
          let user = childSnapshot.val();
          user.key = childSnapshot.key;
          users.push(user);
        });
        return res.send(users);
      })
        .catch(error => {
          res.status(404).send('No data available.');
        });
    });

    exports.users = functions.https.onRequest(app);
```
1. ファンクションを実行
[先ほどの手順](https://qiita.com/items/26de016b0baf60b26c90#%E3%83%95%E3%82%A1%E3%83%B3%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B)でファンクションをデプロイ・実行して問題ないことを確認します。配列で返ってきてますね:thumbsup:
![09.png](https://qiita-image-store.s3.amazonaws.com/0/240974/6f3bb1f9-9737-4c0e-5be4-13b9074e5d45.png)

# その他、CRUDのファンクションを作成
ここまででUserを全件を取得するファンクションを作成しました。続いて以下を作成します。
ここからはソースコードを掲載して簡単に動作確認する程度します。
必要に応じてカスタマイズしてください:robot:

- 特定のUserを1件取得
- Userを作成
- Userを更新
- Userを削除

## 特定のUserを1件取得
### ソースコード
```javascript:index.js
app.get('/:user_id', (req, res) => {
  const ref = admin.database().ref('users');
  ref.orderByChild('user_id').equalTo(req.params.user_id)
    .once('value').then(data => {
      return res.send(data);
    })
    .catch(error => {
      res.status(404).send('No data available.');
    });
});
```

### 確認
User_IdがA0001のユーザーのみ取得します。
![10.png](https://qiita-image-store.s3.amazonaws.com/0/240974/665b72da-244a-807b-ca6b-c18c21f2ed62.png)
## Userを作成
### ソースコード
```javascript:index.js
app.post('/', (req, res) => {
  const ref = admin.database().ref('users');
  ref.push({                                  // ユニークキーも自動生成する
    user_id: req.body.user_id,
    user_name: req.body.user_name,
    age: Number(req.body.age)                 // NaNは考慮しないことにする
  }).then(data => {
    return res.status(201).send(data);
  })
  .catch(error => {
    res.status(400).send(error);      
  });
});
```
### 確認
新しいユーザー「Kurobee」を追加します。
余談ですがREST WebAPIの確認にはChromeアプリの[Advanced REST client](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo?hl=ja)を気に入っています。
![11.png](https://qiita-image-store.s3.amazonaws.com/0/240974/abdb13f5-d367-b366-a0c6-a6c6871db2ea.png)

Userを取得して確認します。
![12.png](https://qiita-image-store.s3.amazonaws.com/0/240974/29b63cfc-41e2-d732-d95c-75e068aab7ca.png)

## Userを更新
### ソースコード
```javascript:index.js
app.put('/:id', (req, res) => {
  const ref = admin.database().ref('users');
  const updates = {};
  updates[req.params.id] = {
    user_id: req.body.user_id,
    user_name: req.body.user_name,
    age: Number(req.body.age)                 // NaNは考慮しないことにする
  };
  ref.update(updates);
  return res.status(204).send("Updated");
});
```
### 確認
ユーザー「Kurobee」の年齢を「3」から「4」に更新します。
![13.png](https://qiita-image-store.s3.amazonaws.com/0/240974/aaecf075-dab0-b2f2-3181-7a97f2528e12.png)
Userを取得して確認します。
![14.png](https://qiita-image-store.s3.amazonaws.com/0/240974/367e1b62-87d0-f075-fa1d-2c6c4d4de55b.png)

## Userを削除
### ソースコード
```javascript:index.js
app.delete('/:id', (req, res) => {
  const ref = admin.database().ref(`users/${req.params.id}`);
  ref.remove().then(data => {
    return res.status(204).send("Deleted");
  })
  .catch(error => {
    res.status(400).send(error);      
  });
});
```
### 確認
ユーザー「Kurobee」を削除します。
![15.png](https://qiita-image-store.s3.amazonaws.com/0/240974/0dd9e0ff-6db5-638b-3af3-c5d8f3e0e30c.png)
Userを取得して確認します。
![スクリーンショット 2019-03-08 2.09.05.png](https://qiita-image-store.s3.amazonaws.com/0/240974/a98eac8d-fbb0-42bf-1801-69faa767f017.png)

# 終わりに
冒頭でもお伝えしましたとおりjsonだけ返却するWebAPIがあれば十分でしたが、つい作成してしまいました。「FaaSやBaaSはサーバーレスで簡単」という触込みが多いですが、それぞれそれなりに独特のものがあると思っています。NoSQLにあまり触れていない筆者はRealtime Databaseに少し苦戦しましたが、簡易的なCRUD WebAPIにはなっていると思いますので、今後活用していくつもりです。