LoginSignup
26
26

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-03-02

前置き

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

なぜFirebaseを選んだか

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

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

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

  3. 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

  2. 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

ファンクションを作成してデプロイする

ようやく本題に入ります。ファンクションのソースコードはGitHubに公開しています。宜しければご覧ください。

  1. プロジェクトを作成したディレクトリに移動
    簡単に構成を確認します。ご察しと思われますがindex.jsにプログラムを書きます。

    ├── firebase.json
    └── functions
        ├── index.js
        ├── node_modules
        ├── package-lock.json
        └── package.json
    
  2. Usersを全件取得するファンクションを作成
    まずは最低限のnpmパッケージのみを利用して動作させてみます。

    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
    2. ブラウザでURLにアクセス
      jsonでUsersの全件が返却されていることを確認します。
      08.png

expressを導入してファンクションを書替える

WebAPIを機能追加するにあたりルーティングを体系的に記述したいのでexpressを導入します。

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

$ 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. ファンクションを実行
    先ほどの手順でファンクションをデプロイ・実行して問題ないことを確認します。配列で返ってきてますね:thumbsup:
    09.png

その他、CRUDのファンクションを作成

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

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

特定のUserを1件取得

ソースコード

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

Userを作成

ソースコード

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を気に入っています。
11.png

Userを取得して確認します。
12.png

Userを更新

ソースコード

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
Userを取得して確認します。
14.png

Userを削除

ソースコード

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
Userを取得して確認します。
スクリーンショット 2019-03-08 2.09.05.png

終わりに

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

26
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
26