Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
25
Help us understand the problem. What is going on with this article?
@devnokiyo

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

More than 1 year has passed since last update.

前置き

外部から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
    
  2. npmをインストール

    $ npm -version
    6.8.0
    
  3. Firebase CLIをインストール

    npm install -g firebase-tools
    
    1. ログイン
      以下のコマンドを実行します。

      $ firebase login
      
    2. そのままEnterを押下

      ? Allow Firebase to collect anonymous CLI usage and error reporting information?
      (Y/n) 
      
    3. ブラウザが起動するのでログイン
      01.png

    4. 諸々承認
      ブラウザでは以下のような表示になっています。
      02.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
  2. セキュリティルールを設定

    今回は「ロックモードで開始」を設定します。
    スクリーンショット 2019-03-02 23.53.35.png

  3. テストデータをインポート
    データ取得用のファンクションの動作確認をするためテストデータを用意します。Firebaseコンソールの画像の赤い部分をクリックして「JSONをインポート」を選択します。
    04.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

  4. インデックスを作成
    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);
      });
    });
    
  3. デプロイ
    functionsディレクトリに移動して、デプロイコマンドを実行します。

    $ 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:

  4. ファンクションを実行

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

    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);
    
  3. ファンクションを実行
    先ほどの手順でファンクションをデプロイ・実行して問題ないことを確認します。配列で返ってきてますね: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にはなっていると思いますので、今後活用していくつもりです。

25
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
devnokiyo
SESから東証1部上場ゲーム会社に転職した正社員エンジニアです。Webサイトや稀にモバイルアプリを自社開発します。採用もかじってます。教材の執筆やAWSスクールのお手伝いもしてます。犬とデジモノが好きです。AP/SC/LPIC 2/オラクルマスターブロンズなら保持してます。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
25
Help us understand the problem. What is going on with this article?