Google Apps ScriptでAPIを作る際のTips

# はじめに

Google Apps Script (GAS) は REST API を作ることができます。
※正確にはGETメソッドとPOSTメソッドに対応したAPIのみ

本記事ではGASでREST APIを作る際に参考になるであろう仕様や機能をいくつか紹介したいと思います。

基本的な内容も多いと思うので、GASについてある程度知っている方は目次を見て「おや？これはあまり知らない情報かも？」という内容だけでも見てもらえればと思います。

また、この記事が参加しているアドベントカレンダーでは実際にGASでAPIを作成したケースもたくさん紹介されているので、興味のある方はぜひご覧ください。

https://qiita.com/advent-calendar/2023/google-apps-script

# APIの作り方

"GAS API" で調べれば作り方の記事がいくらでも出てくるとは思いますが、一応軽く紹介しておきます。

今回はJSONを返すだけのAPIをつくってみます。

まず [Google Apps Scriptのホーム画面](https://script.google.com/home) から「新しいプロジェクト」を立ち上げて、以下のようにdoGet関数とdoPost関数を定義します。

```js:コード.gs
// GETメソッドに対応する関数
function doGet (e) {
  const payload = JSON.stringify({
    method: "GET",
    message: "doGet関数が呼ばれました",
  });
  return ContentService.createTextOutput(payload).setMimeType(ContentService.MimeType.JSON);
}

// POSTメソッドに対応する関数
function doPost(e) {
  const payload = JSON.stringify({
    method: "POST",
    message: "doPost関数が呼ばれました",
  });
  return ContentService.createTextOutput(payload).setMimeType(ContentService.MimeType.JSON);
}
```

次にエディタ画面の右上の方にある「デプロイ」から「新しいデプロイ」を選択し、ウェブアプリとして公開します。

<img src="https://www-tacos.github.io/qiita-images/20231219_gas_api_tips/001.png" alt="新しいデプロイ">

するとウェブアプリのURLが発行されると思いますが、これがAPIのエンドポイントになります。

<img src="https://www-tacos.github.io/qiita-images/20231219_gas_api_tips/002.png" alt="URL発行">

試しに発行されたURLをブラウザのアドレスバーに入れてみるとJSONデータが確認できると思います。

```json
{
  "method": "GET",
  "message": "doGet関数が呼ばれました"
}
```

また、curlコマンドで確認することもできます。
※最近のWindowsであれば curl.exe のパスが通ってるはず

```sh
# doGetの確認
curl -L "発行されたURL"
#-> {"method":"GET","message":"doGet関数が呼ばれました"}

# doPostの確認
curl -d " " -L "発行されたURL"
#-> {"method":"POST","message":"doPost関数が呼ばれました"}
```

といった感じで、GASを使ってAPIを作ることができます。

# doPostの検証方法

[APIの作り方](#apiの作り方) でdoPost関数を確認するときに

```sh
curl -d " " -L "発行されたURL"
```

というコマンドを使っていましたが

* `-L` がいるの？
* `-X` でPOSTメソッドを指定しないの？
* `-d " "` を指定しているのはなんぞ？

と思う人もいるかもしれません。

詳しくは [こちらの記事](https://qiita.com/cajonito/items/9e66ef60831d51105bc0) が参考になると思いますが、GASで作ったAPIへのリクエストはリダイレクトを経由する仕組みなので

* リダイレクトを有効にするために `-L` が必要
* リダイレクト先でGETメソッドを使用するため `-X` が不要
* `-X POST` の代わりにPOSTであることを示すために `-d` が必要

となるみたいです。

なのでcurlに限らずJavaScriptやPythonなどのスクリプトでリクエストを投げるときも上記を意識する必要があるかもしれません。（たいていの場合はデフォルト設定のままで問題なさそう）

# パラメータの取得

リクエストパラメータまたはクエリパラメータはdoGetまたはdoPost関数の引数 `e` (イベントパラメータ) から取得できます。

* リクエストが以下の場合
  * リクエストURL: `https://script.google.com/macros/s/xxxx/exec?msg=Hello&num=12&num=34`
  * POSTのボディデータ: `{ name: "tacos", id: 1 }`

プロパティ | 値
--- | ---
`e.parameter` | `{ msg: "Hello", num: "12" }`
`e.parameters` | `{ msg: ["Hello"], num: ["12", "34"] }`
`e.queryString` | `msg=Hello&num=12&num=34`
`e.postData.contents` | `"{ name: \"tacos\", id: 1 }"`

イベントパラメータのプロパティは他にもありますがひとまず上記を押さえておけば十分かと思います。もっと詳しく知りたい場合は [公式ドキュメント](https://developers.google.com/apps-script/guides/web?hl=ja) を確認してください。

## `parameter` と `parameters` の違い

上記でいうとクエリ文字列で `num=12&num=34` のようにキーが同じパラメータを指定した場合

* `parameter` は `num` に最初に指定した値が設定される（`"12"`）

* `parameters` は指定したすべての値が配列として設定される（`["12","34"]`）

## サブディレクトリ

`https://script.google.com/macros/s/xxxx/exec/hoge/fuga` のように `exec` より後ろでサブディレクトリを切ることができます。

この場合は `e.pathInfo` を用いて `exec/` 配下を取得できます。（`hoge/fuga`）

ただし、サブディレクトリを利用する場合はアクセスできるユーザを全員に設定していたとしても認証が必要になるようです。

https://qiita.com/nmskm240/items/30f55f21081fb09693eb#何故正常に動かないのか

# 制限事項

GASを個人で使う分にはおそらくほとんど意識する必要はないと思いますが、一応実行時間や実行回数などに制限があります。

https://developers.google.com/apps-script/guides/services/quotas?hl=ja

たとえばスクリプトの処理時間が6分を超えたり、スクリプトが同時に1000回以上呼ばれたりするとエラーを返すようになっています。

# HTMLのホスティング

doGet/doPost関数は [HtmlOutput](https://developers.google.com/apps-script/reference/html/html-output?hl=ja) または [TextOutput](https://developers.google.com/apps-script/reference/content/text-output?hl=ja) を返す必要がありますが、この HtmlOutput を使うことでHTMLのホスティングを実現することができます。

返却するHTMLファイルはGASのエディタ上で単一のファイルとして作ることができ、またCSSとJavaScriptもスクリプトレットを利用することで切り分けることができます。

https://qiita.com/taromorimotohf/items/5e52cb9062600e8ccac3

また後述のサンプルにも使っていますが、画像をbase64エンコードしたデータURLをimg要素に設定して返すことで画像を返すAPIを作るみたいこともできますね。

# Googleサービスとの連携

GASは標準でGoogleのいくつかのサービスと連携することができます。

例えばGoogle翻訳やGoogle Mapの機能は以下のように使うことができます。

```js:Google翻訳
function doGet(e) {
  // 日本語から英語に翻訳して返す
  const jaText = e.parameter.text;
  const enText = LanguageApp.translate(text, 'ja', 'en');
  return ContentService.createTextOutput(enText).setMimeType(ContentService.MimeType.TEXT);
}
```

```js:Google Map
function doGet(e) {
  // 東京駅を中心としたマップの画像を返す
  const map = Maps.newStaticMap()
                  .setLanguage("ja")
                  .setSize(1000, 1000)
                  .setZoom(15)
                  .setCenter("東京駅");
  const image = Utilities.base64Encode(map.getAs("image/png").getBytes());
  const imageUrl = `data:image/png;base64,${encodeURI(image)}`;
  return HtmlService.createHtmlOutput().append(`<img src="${imageUrl}">`);
}
```

また、一部のAPIについてはエディタでサービスを追加することで利用可能になります。

<img src="https://www-tacos.github.io/qiita-images/20231219_gas_api_tips/003.png" alt="サービスの追加">

例えば `Drive API` を有効化することで Google Drive のファイル操作を行えたり、`YouTube Data API` を有効化することでYouTubeの動画情報を取得したりできます。

# 排他制御

GASはGoogleスプレッドシートと一緒に使われることが多い印象がありますが、スプレッドシートをDBのように状態の記録に使う場合は排他制御が必要になってきます。

例として、スプレッドシートのA列の最終行の下に値を追加するAPIを考えてみます。

```js
function doPost(e) {
  // 処理1. シートの取得
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート");
  // 処理2. A列の最終行の行数の取得
  const lastRow = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRowIndex();
  // 処理3. A列の最終行の1つ下のセルへの書き込み
  sheet.getRange(lastRow + 1, 1).setValue(e.parameter.name);
}
```

このAPIが複数人から同時に呼ばれた場合どうなるでしょうか？

例えばAさんが呼んだAPIの処理2が実行されたタイミングでBさんが呼んだAPIの処理3が終わってなかった場合、Aさんの処理3とBさんの処理3が対象とするセルが同じになるためどちらかの更新が上書きされてしまいます。

そこで、Aさんの処理3が終わるまで

* ほかのスクリプトが実行できないようする
* シートを更新できないようにする

といった排他制御が必要になります。

GASの排他制御については以下の記事が参考になると思います。

https://qiita.com/kyamadahoge/items/f5d3fafb2eea97af42fe

上記例に排他制御を組み込むと以下のような感じになるかと思います。

```js
function doPost(e) {
  // 処理1. シートの取得
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート");
  // 追加: ロックの選択
  const lock = LockService.getDocumentLock();
  // 追加: 最長で5秒までロックの取得を試みる
  if ( lock.tryLock(1000 * 5) ) {
    // 処理2. A列の最終行の行数の取得
    const lastRow = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRowIndex();
    // 処理3. A列の最終行の1つ下のセルへの書き込み
    sheet.getRange(lastRow + 1, 1).setValue(e.parameter.name);
    // 追加: ロックを解除する
    lock.releaseLock();
  }
}
```

ロック取得を待つ時間については都度設定が可能ですが、前述の制限事項でスクリプトの実行時間は6分までなので、ロック待ち時間は最長でも6分ということになりそうです。

# デバッグ

GASのデバッグ方法には `console.log` と `Logger.log` を使う方法があります。

ただ、API実行の場合はこれらのログはエディタの実行ログには記録されないため別の方法が必要になります。

なので、[こちらの記事](https://prtn-life.com/blog/gas-log-gcp) の引用ですが、大きく分けて3つの対応方をとる必要があります。

## 1. スプレッドシートやドキュメント等の外部ファイルに出力する

サービスAPIを利用してGoogleスプレッドシートに書き込んだり、Google Driveにログファイルを作成したりといった方法です。

手軽さがメリットですが、気を付ける点として、ファイルIOが生じるのでパフォーマンスが悪くなる点と、前述の排他を意識しないといけない点があります。

## 2. GASのプロジェクトをGCPに紐づける

GASのプロジェクトをGCPに紐づけることでGCPのログビューアから過去ログやGASエディタでは確認できないログも見ることができます。（ただし紐づけの手順が面倒）

## 3. doGet/doPost関数を呼び出す関数を作る

開発途中のデバッグであればdoGet/doPost関数にテスト用のイベントデータを渡す関数を作成して呼び出すことでエディタの実行ログを使うことができます。

```js
function doGet(e) {
  Logger.log(e);
  const res = e.parameter.message.repeat(e.parameter.amount);
  Logger.log(res);
  return ContentService.createTextOutput(res);
}

function test() {
  // case1
  doGet({
    parameter: {
      message: "hoge",
      amount: 4
    },
  });
  // case2
  doGet({
    parameter: {
      message: "fuga",
      amount: 1
    },
  });
}
```

# おわりに

ここまで読んでくれたそこのあなた、GASでAPIを作る準備は整ったはずです。

**さぁ、GASでAPIを作ってみましょう！**

...冗談はさておき、GASで作るAPIでできることはかなり多く、特にGoogleサービスとの連携が強力なので、何か作ってみたくなる機会は少なくないと思います。
そのときにこの記事が助けになればうれしいです。

記事は以上になります。最後まで読んでくださりありがとございます。