バックエンド開発１

この記事は、「[架空プロジェクトを通してシステム開発とドキュメント作成を体験してみる（2022 Late）](https://qiita.com/minimumskills/items/aedf6a93658db3f74747)」の記事の一部です。

# 概要

では、バックエンド（サーバ側）の機能を開発していきます。
まず、お問合せページから送信されたデータを受け取り、スプレッドシートに蓄積した機能（API）を作成したのち、その機能に対してデータを送信できるようお問合せページを改修したいと思います。

>APIとはApplication Programming Interfaceの略で、システム間で機能を連携させるための仕組みと理解しておけばいいでしょう。Web記述を利用して提供・連携するAPIを特にWebAPIと言ったりします（ここでは開発するものはWebAPIです）。

![000001.jpg](https://drive.google.com/uc?export=view&id=1HfioXAlma7ScYQCgCPacEMFD7X_tHEky)

今回のAPIはGoogle Apps Scriptで実装します。

> Google Apps ScriptとはGoogleが開発・提供しているプログラミング言語（と環境）です。JavaScriptをベースに設計された言語でGoogleが提供している各種サービスと連携した各種アプリケーションがスムーズに開発できます。今回はスプレッドシートにデータを溜めるためGASとスプレッドシートを連携させて実装することができます。

:::note warn
Google Spreadsheetを利用するにはgoogle アカウントが必要です。
:::

# 完成イメージ

受け取ったデータはGoogle スプレッドシートに蓄積されます。

![000010.jpg](https://drive.google.com/uc?export=view&id=1LPjCTR1JOzV5g35B3HmFIw_Q8dbZt1Qo)

作成したバックエンド機能（API）にデータを送信するようにフロントも改修します。

![000020.jpg](https://drive.google.com/uc?export=view&id=1f7lfGKFmt_MHjhaKvdzGyipUj0YSqlh_)

# API準備
開発するGoogleアカウントにログインして、Google Spreadsheetを開きます。
![000030.jpg](https://drive.google.com/uc?export=view&id=1MEe_XJC9z9B0QRKw198hD-CZS8lplZwG)

「新しいスプレッドシートを作成」から「空白」を選択します。
![000040.jpg](https://drive.google.com/uc?export=view&id=1LuhwAnwJNDC1GdAOJWVdProzgfCEH9uD)

開いたらスプレッドシートの名前が「無題のスプレッドシート」となっているので「問合せ管理シート」としておきます。
![000050.jpg](https://drive.google.com/uc?export=view&id=1NYHO3fP1oLCOBA1jCLS41EJVjc_CFYni)

シートの1行目を見出し行とするため、A1から項目を手入力しておきます。
> 見出し行を設定しておくと、溜まったデータのフィルタリングがスムーズに行えます。

フォームから送られてくる
- 「お名前」
- 「email」
- 「お問合せ内容」

に加えて、

- 問合せの対応ステータスを管理するための「ステータス」
- 問合せがあった日時を記録する「受付日」
- ステータスの更新日時を記録する「更新日」
を追加します。

![000060.jpg](https://drive.google.com/uc?export=view&id=1pwtbC7safXBbTpgbyEGRGxS3XO7Plnld)


Apps ScriptでAPIを実装していきます。

[拡張機能] -> [Apps Script]を開きます。
![000070.jpg](https://drive.google.com/uc?export=view&id=1ZgWqOZHrQ5RKw8TDm2QDRg5YRI1shI7r)

プロジェクトタイトルをわかりやすいものに設定して、スクリプトエディタで以下のように記述します。

>プロジェクト名の変更は左上、Apps Scriptの右隣をクリックすればできます。

![000080.jpg](https://drive.google.com/uc?export=view&id=1F4m0b3q-WbrX1KImunDnfFVZietnb6BC)

ファイル名は「受付API.gs」に変更します。

![000090.jpg](https://drive.google.com/uc?export=view&id=1f0b7qbZNm5hIzaVeVOgzUCbvFDXnUvow)

![000100.jpg](https://drive.google.com/uc?export=view&id=1XgLUSgQG2VaQF2iL9QjJE9-QfacR055E)

> 元々の記述は削除します。

name,email,bodyを受け取り、スプレットシートのシート1にname,email,body、受付日（現在時刻）、更新日（現在日時）をスプレッドシートの一番下の行に追加し、returnに「受け付けました」を返します。


```javascript:受付API.gs
const doPost = (e) => {

    //値の受取り
    const name = e.parameter.name ? e.parameter.name : "";
    const email = e.parameter.email ? e.parameter.email : "";
    const body = e.parameter.body ? e.parameter.body : "";


    //スプレッドシートの準備
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName("シート1");

    //シートの一番下の行に追加
    sheet.appendRow([name, email, body, "受付",new Date(),new Date()]);

    //応答
    return ContentService.createTextOutput("受付けました。");
}
```

これで単純に受け付けてスプレッドシートに登録することはできますが、APIは様々なアプリと連携し叩いてもらうことができるので、今回のフロントエンドの様にバリデーションがきっちりとかかったデータだけが入ってくるとは限りません。
バリデーションがかかっていないデータが入ってきたときに備えてAPI側にも単純なバリデーションを追加します。

```diff_typescript:受付API.gs
const doPost = (e) => {

    //値の受取り
    const name = e.parameter.name ? e.parameter.name : "";
    const email = e.parameter.email ? e.parameter.email : "";
    const body = e.parameter.body ? e.parameter.body : "";

+   //エラー処理
+   const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
+   const body_exp = /^.{1,10}$/;
+
+   //問題があればエラーを返す（なければ処理を継続）
+   if(name == ""|| !email_exp.test(email) || !body_exp.test(body)){
+     return ContentService.createTextOutput("エラーです。");
+   }

    //スプレッドシートの準備
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName("シート1");

    //シートの一番下の行に追加
    sheet.appendRow([name, email, body,"受付",new Date(),new Date()]);

    //応答
    return ContentService.createTextOutput("受付けました。");
}
```

```javascript:コピペ用
//エラー処理
const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
const body_exp = /^.{1,10}$/;

//問題があればエラーを返す（なければ処理を継続）
if(name == ""|| !email_exp.test(email) || !body_exp.test(body)){
  return ContentService.createTextOutput("エラーです。");
}
```

保存して右上から「デプロイ」＞「新しいデプロイ」をクリックします。

![000110.jpg](https://drive.google.com/uc?export=view&id=1j5b3Y2giyqyWzih5Vdc5l4t1MMQTA6MR)

種類の選択から「ウェブアプリ」を選択します。

![000120.jpg](https://drive.google.com/uc?export=view&id=1pHd4wGXRxgkRADSf6vXxffrc_hCKMrsC)

適当な説明を入力して、アクセスできるユーザーを「全員」にします。

![000130.jpg](https://drive.google.com/uc?export=view&id=1sAZxyxUes63NO_iQubg8dBq_ghCct8bE)



> 技術的にはAccess-Control-Allow-Orign:"*"となるようです。
> 実運用ではAPIを全ユーザーに認証無しで公開することはありません。Tokenを利用した認証機能などを実装します。

準備ができたら[デプロイ]を押します。


すると、Spreadsheetにアクセスするためのアクセス認証ボタンが表示されます。

![000140.jpg](https://drive.google.com/uc?export=view&id=13nLVYOplRjkC-vh7TyKwmw4puUHsR0E7)

「アクセスを承認」をクリックします。

次に承認に利用するGoogleアカウントを選択する画面が表示されるので適切なアカウントを選択します。

![000150.jpg](https://drive.google.com/uc?export=view&id=10huKtmqqHYQi39c2gs5Nd4pzwtZIRlel)

:::note warn
「このアプリは確認されていません」と表示された場合

Google Apps Script のセキュリティ強化によりレビューを受けたアプリ以外からの Google Drive などの操作を原則弾くようになったことが原因で表示されます。

**対処方法**は、メッセージ左下に表示されている「詳細」から「xxxxx（安全ではないページ）に遷移」をクリックすることで次に進めます。
<img width="300" alt="000160.jpg" src="https://drive.google.com/uc?export=view&id=1_k1BKbAJjmIeVJyIQLxqpUzAKgnh3N4j"><img width="300" alt="000170.jpg" src="https://drive.google.com/uc?export=view&id=156PsxSVaVw7Bo0hmyyrG159nebgK5-lI">
:::

作成したプロジェクトがスプレッドシートの参照・編集・作成・削除を行うことへの許可を確認されるので「許可（Allow）」を選択します。

![000180.jpg](https://drive.google.com/uc?export=view&id=13-AyYapmjBJS-7kcGMmPv0S0bUo3XJ5U)


デプロイが完了すると、ウェブアプリのURLが表示されるのでコピーしておきます。このURLはあとからデプロイの管理から確認できます。
完了をクリックしてウインドウを閉じます。

![000190.jpg](https://drive.google.com/uc?export=view&id=1Ojz5gQL3bAy8j3O2b4fpTbB6zbE_6ZxV)

# API連携（フロント改修）

APIを呼び出すコードは、送信ボタンが押されalertを表示していた部分に記述をしていきます。
Fetch APIのfetch関数を使うことで非同期通信で簡単にHTTPリクエストでき、レスポンスも受け取れます。

> 非同期通信とは、リクエストを送り、そのレスポンスを待つことなく次の処理を行うことができる通信のことです。（同期通信は、リクエストのレスポンスが返ってきてから次の処理を実行します）

fetch関数 は、第一引数にリクエストURL、第二引数にリクエストオプション（下記ではmethodとheadersとbody）を指定します。
fetchの戻り値はPromise で実装されているので、then() で Responseのオブジェクトを受け取って成功の場合の処理をします。
エラーの場合、Error() オブジェクトが投げられるので、catch() に処理に移ります。

```javascript:fetchの形式
fetch(第一引数,第二引数)
.then((response) => 成功の場合の処理 )
.catch((error) => エラーの場合の処理)
```

```javascript:fetchの具体例
fetch(リクエストURL,{
    method:"postやget",
    headers:"コンテントタイプなど"
    body:"パラメータ"
})
.then((response) => 成功の場合の処理)
.catch((error) => エラーの場合の処理)
```

これを今回の内容に照らし合わせると以下の様になります。
bodyにはURLには使用できない文字が入る可能性があるのでencodeURI関数を使用してencodeしています。
then()でサーバーから応答を受信するとPromise インスタンスは Response のオブジェクトで解決されるので、Responseを受け取りresponse.text()を使ってレスポンスの内容をテキストとして解決するPromiseとして返します。
次のthen()でtextを受け取り、内容をalertで表示します。

```diff_javascript:index.js
    const submit_btn = document.getElementById("submit_btn");

    const contact_name = document.getElementById("name");
    const email = document.getElementById("email");
    const body = document.getElementById("body");

    const name_error = document.getElementById("name_error");
    const email_error = document.getElementById("email_error");
    const body_error = document.getElementById("body_error");

    const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
    const body_exp = /^.{1,10}$/;

+   //サーバにデータを送信（{deploy_id}は各自の環境に依存）
+   const api_url = "https://script.google.com/macros/s/{デプロイID}/exec";

    submit_btn.addEventListener("click", (e) => {
        e.preventDefault();

        if (contact_name.value == "") {
            name_error.classList.remove("hidden");
        }

        if (!email_exp.test(email.value)) {
            email_error.classList.remove("hidden");
        }

        if (!body_exp.test(body.value)) {
            body_error.classList.remove("hidden")
        }

        if (name_error.classList.contains("hidden") && email_error.classList.contains("hidden") && body_error.classList.contains("hidden")) {
-           alert(`お名前：${contact_name.value}\nemail：${email.value}\nお問合せ内容：${body.value}`);
+            fetch(api_url, {
+                method: "post",
+                headers: {
+                    "Content-Type": "application/x-www-form-urlencoded"
+                },
+                body: encodeURI(`name=${contact_name.value}&email=${email.value}&body=${body.value}`)
+            })
+                .then((response) => {
+                    response.text().then((text) => {
+                        alert(text);
+                    });
+                })
+                .catch((error) => {
+                    alert(error.message);
+                });
        }

    });
```
```javascript:コピペ用
fetch(api_url, {
    method: "post",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded"
    },
    body: encodeURI(`name=${contact_name.value}&email=${email.value}&body=${body.value}`)
})
    .then((response) => {
        response.text().then((text) => {
            alert(text);
        });
    })
    .catch((error) => {
        alert(error.message);
    });
```
保存してお問合せフォームの動作確認をします。
「受け付けました。」とアラートが表示され、スプレッドシートを確認すると、データが入っていることが確認できます。

![000200.jpg](https://drive.google.com/uc?export=view&id=13f61Ddrjxfk3UNhIAxGhBSeuwmIEe_7c)


# 戻り値をJSONに変更

## バックエンド側

APIの戻り値をテキスト（普通の文字列）で戻していましたが、一般的にはJSONという形式で戻すことが多いのでJSONに形式を変更します。

> JSONとはJavaScript Object Notationの略で、もとはJavaScriptでデータを扱うためのフォーマットでしたが、バックエンドと通信する際のフォーマットとして利用されるようになりました。テキストベースでシンプルな記述のため、データ量が軽量で、データの加工がしやすく、わかりやすい形式となっています。

JSONオブジェクトを文字列化してreturnします。

JSONオブジェクトを文字列化するには`JSON.stringify`を使います。
`{"message":"メッセージの内容"}`のという形のJSONオブジェクトを`JSON.stringify()`で囲むことで文字列に変換します。

```json:サンプル
JSON.stringify({"message":"メッセージの内容"})
```
これを今回の内容に照らし合わせると以下の様になります。

```diff_javascript:受付API.gs
	const doPost = (e) => {

	    //値の受取り
	    const name = e.parameter.name ? e.parameter.name : "";
	    const email = e.parameter.email ? e.parameter.email : "";
	    const body = e.parameter.body ? e.parameter.body : "";

	    //エラー処理
	    const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
	    const body_exp = /^.{1,10}$/;

	    //問題があればエラーを返す（なければ処理を継続）
	    if(name == ""|| !email_exp.test(email) || !body_exp.test(body)){
-	      return ContentService.createTextOutput("エラーです。");
+	      return ContentService.createTextOutput(JSON.stringify({ "message": "validation error!" }));
	    }

	    //スプレッドシートの準備
	    const ss = SpreadsheetApp.getActiveSpreadsheet();
	    const sheet = ss.getSheetByName("シート1");

	    //シートの一番下の行に追加
	    sheet.appendRow([name, email, body,"受付",new Date(),new Date()]);

	    //応答
-	    return ContentService.createTextOutput("受付けました。");
+	    return ContentService.createTextOutput(JSON.stringify({ "message": "success!" }));
	}
```
保存したら、「デプロイ」＞「新しいデプロイ」をクリックし、説明文に適当な説明を入れ、アクセスできるユーザーが「全員」になっていることを確認して「デプロイ」をします。
デプロイIDをコピーしておきます。

> デプロイは何度でも実行できます。
「デプロイ」＞「デプロイを管理」からそれぞれのバージョンのデプロイIDを確認することができます。


## フロント側
受け取る方も変更します。
内容をJSONで受け取ります。jsonオブジェクト内のキーのmessageの内容をアラートで表示したいので`json.message`の値を`alert()`に設定します。

:::note warn
デプロイIDが変更になっているので、api_urlのデプロイIDも忘れず変更します。
:::

```diff_javascript:index.js
    if (name_error.classList.contains("hidden") && email_error.classList.contains("hidden") && body_error.classList.contains("hidden")) {
        fetch(api_url, {
            method: "post",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            },
            body: encodeURI(`name=${contact_name.value}&email=${email.value}&body=${body.value}`)
        })
            .then((response) => {
-               response.text().then((text) => {
-                   alert(text);
-               });
+               response.json().then((json) => {
+                   alert(json.message);
+               });
            })
            .catch((error) => {
                alert(error.message);
            });
    }
```

```javascript:コピペ用
response.json().then((json) => {
     alert(json.message);
});
```

保存して実行してみます。
![000210.jpg](https://drive.google.com/uc?export=view&id=1kbeyDb0Sygda98w_eAE6z06SFh5DP8XX)

:::note info
今回はスプレッドシートの行数を一意な番号として利用していますが、スプレッドシートは行が削除されると番号が繰り上がって変更されてしまう仕様となるため、通常はカラムを追加して一意な番号が自動で振られる機能を実装する必要があります。
今回は目的から外れるため割愛していますが、方法が知りたい方はこちらの記事を参考にしてみてください。
[GASでスプレッドシートに自動採番を追加する](https://qiita.com/minimumskills/items/a72ce71246952d2e6261)
:::


# 最終的なコード

```index.js
const submit_btn = document.getElementById("submit_btn");

const contact_name = document.getElementById("name");
const email = document.getElementById("email");
const body = document.getElementById("body");

const name_error = document.getElementById("name_error");
const email_error = document.getElementById("email_error");
const body_error = document.getElementById("body_error");

const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
const body_exp = /^.{1,10}$/;

const api_url = "https://script.google.com/macros/s/{デプロイID}/exec";

submit_btn.addEventListener("click", (e) => {
    e.preventDefault();

    if (contact_name.value == "") {
        name_error.classList.remove("hidden");
    }

    if (!email_exp.test(email.value)) {
        email_error.classList.remove("hidden");
    }

    if (!body_exp.test(body.value)) {
        body_error.classList.remove("hidden")
    }

    if (name_error.classList.contains("hidden") && email_error.classList.contains("hidden") && body_error.classList.contains("hidden")) {
        fetch(api_url, {
            method: "post",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            },
            body: encodeURI(`name=${contact_name.value}&email=${email.value}&body=${body.value}`)
        })
            .then((response) => {
                response.json().then((json) => {
                    alert(json.message);
                });
            })
            .catch((error) => {
                alert(error.message);
            });
    }

});

contact_name.addEventListener("keyup", (e) => {
    if (contact_name.value == "") {
        name_error.classList.remove("hidden");
    } else {
        name_error.classList.add("hidden");
    }
});

email.addEventListener("keyup", (e) => {
    if (!email_exp.test(email.value)) {
        email_error.classList.remove("hidden");
    } else {
        email_error.classList.add("hidden");
    }
})

body.addEventListener("keyup", (e) => {
    if (!body_exp.test(body.value)) {
        body_error.classList.remove("hidden");
    } else {
        body_error.classList.add("hidden");
    }
})
```

```javascript:受付API.gs
const doPost = (e) => {

    //値の受取り
    const name = e.parameter.name ? e.parameter.name : "";
    const email = e.parameter.email ? e.parameter.email : "";
    const body = e.parameter.body ? e.parameter.body : "";

    //エラー処理
    const email_exp = /^[a-z0-9.]+@[a-z0-9.]+\.[a-z]+$/;
    const body_exp = /^.{1,10}$/;

    //問題があればエラーを返す（なければ処理を継続）
    if(name == ""|| !email_exp.test(email) || !body_exp.test(body)){
      return ContentService.createTextOutput(JSON.stringify({ "message": "validation error!" }));
    }

    //スプレッドシートの準備
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName("シート1");

    //シートの一番下の行に追加
    sheet.appendRow([name, email, body,"受付",new Date(),new Date()]);

    //応答
    return ContentService.createTextOutput(JSON.stringify({ "message": "success!" }));
}
```

# まとめ

* バックエンドとはサーバ側とかの機能（群）意味する
  * バックエンドをサーバサイドと呼んだりする（フロントエンドはクライアントサイドと呼んだりする）
* フロントエンドとバックエンドの連携はAPIを介して行われることが多い（その他の方法もあります）
  * (Web)API間でのデータのやり取りはJSON形式が使われることが多い
* 今回はGoogle Apps Script（GAS）というGoogleの提供する仕組みで開発をした
  * このようなサーバを構築しない方式をサーバレスアーキテクチャとも言う
  * 一般的にはLinuxで（仮想）サーバを立てたりする
  * データの保存にはリレーショナルデータベースが利用されることが多い（MySQL、MS-SQL, Oracle等）


# ドキュメント作成視点での考察

* APIの仕様はどこにどう記述すべきか？
* そもそもAPIで連携することをどこにどう記述すべきか？
* Google Apps Scriptを利用することをどこにどう記述すべきか？
* Google Apps Scriptを利用することをどこでどう決定するか？

# 関連コンテンツ

* 次：[動作確認（サービスの公開）](https://qiita.com/minimumskills/items/e9661e5ea233d060ce83)
* 前：[フロントエンド開発３](https://qiita.com/minimumskills/items/f74b14e04285f52b999d)