GAS(Google Apps Script)のWebアプリは、スクリプトに doGet(e) または doPost(e) という関数を書いてデプロイすると、URLへの GET・POST リクエストでその関数が動く仕組みです。 JSONを返すときは、ContentService.createTextOutput に文字列を渡し、setMimeType で JSON を指定した TextOutput を返します。

この記事は「リクエストを受け取る側」に絞り、イベントオブジェクトの一覧表、JSONを返す最小コード、スプレッドシートを参照・追加するAPIの完成コード、デプロイ設定の組み合わせ表、/exec と /dev の違い、302 リダイレクトに合わせた curl・fetch の書き方、エラー応答の形までを順にまとめます。

確認環境:仕様は Google Apps Script 公式ドキュメント(developers.google.com/apps-script、2026-10-05 閲覧)の記載に合わせています。掲載した doGet・doPost は、Google のサービスを模した簡易スタブを用意して Node.js 22.22 で動かし、返すJSONを確かめました。リダイレクトの挙動は、302 を返す手元のサーバーに対して curl 8.5.0 と Node.js 22.22 の fetch で確かめています。

doGet・doPostの早見表

やりたいこと 書き方
GET を受け取る function doGet(e) { ... }
POST を受け取る function doPost(e) { ... }
クエリの値を読む(?name=taro) e.parameter.name
同じ名前の値を全部読む(?n=1&n=2) e.parameters.n(配列)
クエリ文字列をそのまま読む e.queryString
POST の本文を読む e.postData.contents
POST の本文の種類を読む e.postData.type
JSON の本文をオブジェクトにする JSON.parse(e.postData.contents)
JSON を返す ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON)
テキストを返す ContentService.createTextOutput('ok')
公開する Deploy → New deployment → 種類で Web app を選ぶ → Deploy
コードの変更を公開URLに反映する Deploy → Manage deployments → Edit → 新しいバージョンを選ぶ → Deploy
保存した最新コードを試す Deploy → Test deployments の URL(末尾が /dev)
curl で GET curl -L "https://script.google.com/macros/s/デプロイID/exec?name=taro"
curl で POST curl -L -H "Content-Type: application/json" -d '{"name":"pen"}' "…/exec"

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「GAS WebアプリのdoGet・doPost|デプロイ方法とJSONの返し方」

Webアプリになる条件と全体の流れ

公式ドキュメントでは、スクリプトをWebアプリとして公開できる条件を次の2つとしています。

  1. doGet(e) または doPost(e) という関数がある。
  2. その関数が、HTML service の HtmlOutput か、Content service の TextOutput を返す。

スプレッドシートなどに紐づいたスクリプトでも、単独のスクリプトでも公開できます。JSONを返すAPIとして使うときは TextOutput を返します。

sequenceDiagram
    participant C as 呼び出す側(curl・fetch)
    participant G as script.google.com(/exec)
    participant U as script.googleusercontent.com
    C->>G: GET または POST
    Note over G: doGet(e) / doPost(e) が動く
    G-->>C: 302(Location に一度だけ使えるURL)
    C->>U: GET(リダイレクト先)
    U-->>C: 200 と JSON

Content service の公式ガイドには、セキュリティ上の理由から、Content service が返す内容は script.googleusercontent.com の一度だけ使えるURLへリダイレクトされると書かれています。呼び出す側はリダイレクトをたどる設定にしておく必要があります(書き方は後半で扱います)。

イベントオブジェクト e の一覧

doGet・doPost の引数 e には、リクエストの内容が入っています。公式ドキュメントの表を日本語にまとめると次のとおりです。

フィールド 内容 例
e.queryString URLのクエリ文字列。無いときは null name=alice&n=1&n=2
e.parameter パラメータ名と値の組。同じ名前が複数あるときは最初の値だけ {"name": "alice", "n": "1"}
e.parameters e.parameter と同じ形で、値が配列 {"name": ["alice"], "n": ["1", "2"]}
e.pathInfo /exec または /dev の後ろのパス URLが /exec/hello で終わるとき hello
e.contextPath 使われていない。常に空文字列 ""
e.contentLength POST の本文の長さ。GET では -1 332
e.postData.length e.contentLength と同じ 332
e.postData.type POST の本文の MIME タイプ text/csv
e.postData.contents POST の本文の文字列 Alice,21
e.postData.name 常に "postData" postData

読むときのポイントは3つです。

  • e.parameter の値はすべて文字列です。 数値として使うときは Number(e.parameter.qty) のように変換します。
  • JSON を POST したときの中身は e.postData.contents に文字列で入ります。 JSON.parse でオブジェクトに直して使います。
  • パラメータ名の c と sid は予約されています。 公式ドキュメントには、これらをURLパラメータや POST の本文に使うと HTTP 405 と「Sorry, the file you have requested does not exist.」が返ることがあると書かれています。

JSONを返す最小コード

まず、受け取った値をそのまま返すだけの最小形です。

function doGet(e) {
  const name = e.parameter.name || 'world';
  return json_({ ok: true, message: 'hello ' + name });
}

function doPost(e) {
  const body = JSON.parse(e.postData.contents);
  return json_({ ok: true, received: body, type: e.postData.type });
}

function json_(data) {
  return ContentService.createTextOutput(JSON.stringify(data))
    .setMimeType(ContentService.MimeType.JSON);
}

GET で ?name=taro を付けたとき、POST で {"name":"pen"} を送ったときに返すJSONは次のとおりです(スタブで確認)。

{"ok":true,"message":"hello taro"}
{"ok":true,"received":{"name":"pen"},"type":"application/json"}

TextOutput で使うメソッドは次のとおりです。

メソッド 内容
ContentService.createTextOutput(content) 文字列から TextOutput を作る
setMimeType(mimeType) 返す内容の種類を指定する
setContent(content) / append(addedContent) 内容を設定する/追記する
getContent() / getMimeType() 今の内容/種類を読む
downloadAsFile(filename) ブラウザーに表示ではなくダウンロードさせる
clear() 内容を消す

公式ガイドによると、Content service で返せる種類は ATOM、CSV、iCal、JavaScript、JSON、RSS、vCard、XML です。JSONを返すときは、公式サンプルと同じく setMimeType(ContentService.MimeType.JSON) を付けます。

スプレッドシートを参照・追加するAPIの完成コード

備品の一覧を持つシートを、参照(GET)と追加(POST)ができるAPIにします。

準備するシート

スプレッドシートに items という名前のシートを作り、1行目に見出しを入れます。

id name qty createdAt
1 ノート 10 2026-10-01T00:00:00.000Z
2 ペン 25 2026-10-02T00:00:00.000Z

貼り付け用(タブ区切り):

id  name    qty createdAt
1   ノート 10  2026-10-01T00:00:00.000Z
2   ペン  25  2026-10-02T00:00:00.000Z

コード全体

スプレッドシートの「拡張機能 → Apps Script」で開いたエディタに、次のコードを貼ります。

const SHEET_NAME = 'items';

function doGet(e) {
  try {
    const rows = readItems_();
    const id = e.parameter.id;
    if (id === undefined) {
      return json_({ ok: true, count: rows.length, items: rows });
    }
    const item = rows.find((r) => String(r.id) === String(id));
    if (!item) {
      return error_('NOT_FOUND', 'id=' + id + ' は見つかりません');
    }
    return json_({ ok: true, item: item });
  } catch (err) {
    return error_('INTERNAL', err.message);
  }
}

function doPost(e) {
  let body;
  try {
    body = JSON.parse(e.postData.contents);
  } catch (err) {
    return error_('BAD_JSON', '本文をJSONとして読み取れません');
  }

  const token = PropertiesService.getScriptProperties().getProperty('API_TOKEN');
  if (token && body.token !== token) {
    return error_('UNAUTHORIZED', 'token が一致しません');
  }
  if (typeof body.name !== 'string' || body.name === '') {
    return error_('VALIDATION', 'name は必須です');
  }
  const qty = Number(body.qty);
  if (!Number.isInteger(qty) || qty < 0) {
    return error_('VALIDATION', 'qty は0以上の整数で指定します');
  }

  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10000)) {
    return error_('BUSY', '混み合っています。少し待って再送してください');
  }
  try {
    const sheet = getSheet_();
    const id = sheet.getLastRow(); // 見出し行があるので、最終行の番号が次のidになる
    const now = new Date().toISOString();
    sheet.appendRow([id, body.name, qty, now]);
    return json_({ ok: true, item: { id: id, name: body.name, qty: qty, createdAt: now } });
  } catch (err) {
    return error_('INTERNAL', err.message);
  } finally {
    lock.releaseLock();
  }
}

function getSheet_() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error('シート ' + SHEET_NAME + ' がありません');
  }
  return sheet;
}

function readItems_() {
  const values = getSheet_().getDataRange().getValues();
  const header = values[0];
  return values.slice(1).map((row) => {
    const obj = {};
    header.forEach((key, i) => {
      obj[key] = row[i];
    });
    return obj;
  });
}

function json_(data) {
  return ContentService.createTextOutput(JSON.stringify(data))
    .setMimeType(ContentService.MimeType.JSON);
}

function error_(code, message) {
  return json_({ ok: false, error: { code: code, message: message } });
}

コードの要点は次のとおりです。

箇所 ねらい
readItems_ getDataRange().getValues() で全体を1回で読み、見出し行をキーにしたオブジェクトの配列に直す
JSON.parse を try で囲む JSON でない本文が来たときに、決まった形のエラーを返す
API_TOKEN スクリプト プロパティに値を入れておくと、同じ値を token に入れたリクエストだけ受け付ける。未設定なら確認を省く
LockService.getScriptLock() 同時に届いた POST が同じ id を採番しないよう、追加の間だけ順番待ちにする。tryLock は時間内に取れなければ false を返す
finally で releaseLock 途中で例外が出てもロックを返す

返すJSON(スタブで確認)

上の2行が入った状態で、リクエストごとに返すJSONは次のとおりです。API_TOKEN には abc を設定しています。

リクエスト 返すJSON
GET(パラメータなし) {"ok":true,"count":2,"items":[{"id":1,"name":"ノート","qty":10,"createdAt":"2026-10-01T00:00:00.000Z"},{"id":2,"name":"ペン","qty":25,"createdAt":"2026-10-02T00:00:00.000Z"}]}
GET ?id=2 {"ok":true,"item":{"id":2,"name":"ペン","qty":25,"createdAt":"2026-10-02T00:00:00.000Z"}}
GET ?id=9 {"ok":false,"error":{"code":"NOT_FOUND","message":"id=9 は見つかりません"}}
POST {"token":"abc","name":"付箋","qty":5} {"ok":true,"item":{"id":3,"name":"付箋","qty":5,"createdAt":"2026-10-05T01:23:45.678Z"}}
POST name=pen(JSONでない) {"ok":false,"error":{"code":"BAD_JSON","message":"本文をJSONとして読み取れません"}}
POST {"token":"abc","qty":5} {"ok":false,"error":{"code":"VALIDATION","message":"name は必須です"}}
POST {"token":"abc","name":"x","qty":-1} {"ok":false,"error":{"code":"VALIDATION","message":"qty は0以上の整数で指定します"}}
POST {"token":"zzz","name":"x","qty":1} {"ok":false,"error":{"code":"UNAUTHORIZED","message":"token が一致しません"}}
items シートが無い {"ok":false,"error":{"code":"INTERNAL","message":"シート items がありません"}}

デプロイの手順

公式ドキュメントの手順は次のとおりです(項目名はドキュメントの英語表記。画面の表示言語が日本語のときは、対応する項目を選びます)。

  1. スクリプト エディタの右上で Deploy → New deployment をクリックする。
  2. 「Select type」の横にある Enable deployment types(設定のアイコン)から Web app を選ぶ。
  3. 「Deployment configuration」に、実行ユーザー(Execute as)やアクセスできるユーザーなどを入力する。
  4. Deploy をクリックする。表示された Web app の URL(末尾が /exec)を控える。

実行ユーザーとアクセスできるユーザーの組み合わせ

設定は2つの軸の組み合わせです。マニフェスト(appsscript.json)の webapp に書く値と、公式ドキュメントの説明は次のとおりです。

軸 マニフェストの値 意味
実行ユーザー(executeAs) USER_DEPLOYING デプロイした人として実行する(画面では Execute the app as me)
実行ユーザー(executeAs) USER_ACCESSING アクセスした人として実行する(画面では Execute the app as user accessing the web app)
アクセス(access) MYSELF デプロイした人だけが実行できる
アクセス(access) DOMAIN デプロイした人と同じドメインのユーザーだけが実行できる
アクセス(access) ANYONE ログインしているユーザーなら誰でも実行できる
アクセス(access) ANYONE_ANONYMOUS ログインしていなくても誰でも実行できる

この定義から、よく使う組み合わせを整理すると次のようになります。

実行ユーザー アクセス シートを読み書きする権限 向いている用途
USER_DEPLOYING MYSELF デプロイした人 自分だけで試す
USER_DEPLOYING DOMAIN デプロイした人 社内の人がブラウザーから使う。シート自体は共有しなくてよい
USER_DEPLOYING ANYONE デプロイした人 Google アカウントでログインした人に広く使ってもらう
USER_DEPLOYING ANYONE_ANONYMOUS デプロイした人 curl や他のプログラムからログインなしで呼ぶ API
USER_ACCESSING DOMAIN / ANYONE アクセスした人 利用者ごとに、その人が持つ権限の範囲で動かす
  • USER_DEPLOYING では、誰がアクセスしてもスクリプトはデプロイした人の権限で動きます。 利用者にシートを共有していなくても、スクリプト経由で読み書きできます。公開範囲を広げるほど、コード側での入力チェックと token などの確認が大切になります。
  • USER_ACCESSING では、アクセスした人の権限で動きます。 公式ドキュメントによると、この方式では利用者が承認する画面にスクリプトのオーナーのメールアドレスが表示され、必要な権限(スコープ)を選んで許可する画面になります。
  • 公式ドキュメントは、デプロイした人として実行するWebアプリで ScriptApp.getOAuthToken() のトークンを扱うときは十分に注意し、クライアントへ送らないよう呼びかけています。

マニフェストに書く場合の例です。

{
  "timeZone": "Asia/Tokyo",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "ANYONE_ANONYMOUS"
  }
}

/execと/devの違いと、コードを直したあとの反映

/exec(デプロイのURL) /dev(テスト用URL)
開き方 Deploy → New deployment で発行 Deploy → Test deployments で表示
動くコード デプロイに結び付けたバージョン 保存した最新のコード
アクセスできる人 デプロイ設定で決めた範囲 スクリプトを編集できるユーザーだけ
用途 利用者・他のプログラムに渡す 開発中の動作確認

/exec は「デプロイした時点のバージョン」を動かし続けます。コードを保存しただけでは /exec の動きは変わりません。 反映するには、公式ドキュメント「Create and manage deployments」の次の手順で、同じデプロイを新しいバージョンに更新します。

  1. Deploy → Manage deployments を開く。
  2. 対象のデプロイを選び、Edit をクリックする。
  3. バージョンで新しいバージョンを作成して選ぶ。
  4. Deploy をクリックする。

この手順なら、デプロイIDとURLはそのままで、中身のバージョンだけが入れ替わります。 一方、New deployment をもう一度行うと別のデプロイが増え、呼び出す側に配るURLも別のものになります。バージョン付きのデプロイは削除ではなくアーカイブで片づけます。なお、公式の上限一覧では、1つのスクリプトが持てるバージョンは200までです。

302リダイレクトに合わせたcurl・fetchの書き方

前半の図のとおり、/exec へのリクエストには 302 が返り、Location のURLを GET すると本文が得られます。この動きに合わせて、呼び出す側を次のように書きます。

curl

公式ガイドは、curl では -L を付けるよう案内しています。

# GET
curl -L "https://script.google.com/macros/s/デプロイID/exec?id=2"

# POST(JSON)
curl -L \
  -H "Content-Type: application/json" \
  -d '{"token":"abc","name":"付箋","qty":5}' \
  "https://script.google.com/macros/s/デプロイID/exec"

POST では -X POST を付けず、-d だけで POST にします。 302 のあとの curl の動きを、302 を返す手元のサーバー(http://127.0.0.1:9710/exec が /echo へ案内する)で確かめた結果が次の表です。

コマンド 1回目 リダイレクト先への2回目
curl URL(-L なし) GET 送らない(302 を受け取って終わり)
curl -L URL GET GET
curl -L -d '…' URL POST(本文あり) GET
curl -L -X POST -d '…' URL POST(本文あり) POST(本文なし)

-X POST を付けると、リダイレクト先にも POST で取りに行きます。本文を受け取るための2回目は GET にしたいので、-d だけの形が合っています。

fetch(Node.js 22)

fetch は既定でリダイレクトをたどります(redirect の既定値は follow)。

const url = 'https://script.google.com/macros/s/デプロイID/exec';

const res = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token: 'abc', name: '付箋', qty: 5 }),
});
const data = await res.json();

if (!data.ok) {
  throw new Error(data.error.code + ': ' + data.error.message);
}
console.log(data.item);

同じ手元のサーバーに対して Node.js 22.22 で確かめると、既定の設定では2回目が GET になり、res.status は 200、res.redirected は true でした。redirect: 'manual' を指定した場合は 302 のまま返り、Location ヘッダーを自分で読む形になります。

既定(follow)   : status=200 redirected=true  リダイレクト先へは GET
redirect:'manual': status=302 location=/echo?id=1

エラーは本文の ok と error で伝える

TextOutput のメソッドは前述の表のとおりで、HTTP のステータスコードやレスポンス ヘッダーを設定するメソッドはリファレンスにありません。 そのため、入力の誤りや「見つからない」を 400 や 404 で伝える作りにはせず、本文のJSONに成否を入れる形にそろえます。

状況 返す本文
成功 {"ok": true, "item": { ... }}
入力の誤り {"ok": false, "error": {"code": "VALIDATION", "message": "name は必須です"}}
対象が無い {"ok": false, "error": {"code": "NOT_FOUND", "message": "..."}}
認証が通らない {"ok": false, "error": {"code": "UNAUTHORIZED", "message": "..."}}
混雑 {"ok": false, "error": {"code": "BUSY", "message": "..."}}
想定外の例外 {"ok": false, "error": {"code": "INTERNAL", "message": "..."}}

決めておくとよいルールは次の3つです。

  • 最上位に必ず ok(真偽値)を入れる。 呼び出す側は、まず ok を見て分岐します。
  • error.code は機械が判定する固定の文字列、error.message は人が読む文章にする。
  • doGet・doPost の処理を try…catch で囲み、例外もJSONで返す。 どの場合も同じ形で返しておくと、呼び出す側は常に同じ手順で解析できます。

動かないときの確認表

起きること 確認する点
コードを直したのに /exec の結果が変わらない Manage deployments → Edit で新しいバージョンに更新したか
curl の結果にJSONが出てこない -L を付けたか(302 の先を取りに行く必要がある)
ログインしていないプログラムから呼べない アクセスの設定が、呼び出し方に合っているか(ログインなしで呼ぶなら ANYONE_ANONYMOUS)
/dev のURLを他の人に渡したら開けない /dev はスクリプトを編集できるユーザー専用。渡すのは /exec
doGet で本文を読もうとしてエラーになる GET には本文が無い(e.contentLength は -1)。doGet では e.parameter を使う
数値の比較が合わない e.parameter の値は文字列。Number で変換する
HTTP 405 が返る パラメータ名に c や sid を使っていないか
同じ id の行ができる 追加の処理を LockService で囲んだか

実行数に関する上限は、公式の上限一覧で「ユーザーごとの同時実行は30」「スクリプトごとの同時実行は1,000」「1回の実行は6分」とされています。呼び出しが集中する用途では、呼び出す側で間隔を空ける・BUSY のときに待って再送する、といった作りにしておくと安定します。

よくある質問

Q. doGet と doPost の両方を書く必要がありますか? A. どちらか一方があれば公開できます。GET だけ受け付けるなら doGet だけで構いません。

Q. PUT や DELETE は受け取れますか? A. 公式ドキュメントに書かれているのは、GET で doGet、POST で doPost が動くという2つです。更新や削除を作るときは、POST の本文に action のような項目を入れて処理を分ける方法が使えます。

Q. URLを変えずにコードを更新できますか? A. できます。Manage deployments で既存のデプロイを Edit し、新しいバージョンを選んで Deploy します。デプロイIDとURLは同じままです。

Q. /exec/items のようなパスで処理を分けられますか? A. /exec の後ろのパスは e.pathInfo に入ります。値を見て分岐すれば、1つのWebアプリで複数の入口を用意できます。

まとめ

  • doGet(e)・doPost(e) を書き、TextOutput を返す関数にしてデプロイすると Webアプリになる。
  • クエリは e.parameter(文字列)、同名の複数値は e.parameters、POST の本文は e.postData.contents。
  • JSON は ContentService.createTextOutput(JSON.stringify(obj)) に setMimeType(ContentService.MimeType.JSON)。
  • デプロイ設定は「実行ユーザー × アクセスできるユーザー」。ログインなしで呼ぶ API は USER_DEPLOYING と ANYONE_ANONYMOUS。
  • /exec はデプロイしたバージョン、/dev は保存した最新コード。直したら Manage deployments で新しいバージョンに更新する。
  • 応答は 302 でリダイレクトされる。curl は -L と -d、fetch は既定のままでたどる。
  • ステータスコードは変えられない前提で、本文の ok と error.code で成否を伝える。

参考・出典

  • Google Apps Script「Web apps」 https://developers.google.com/apps-script/guides/web
  • Google Apps Script「Content service」 https://developers.google.com/apps-script/guides/content
  • Google Apps Script「Create and manage deployments」 https://developers.google.com/apps-script/concepts/deployments
  • Google Apps Script「Manifest structure: Web app and API executable」 https://developers.google.com/apps-script/manifest/web-app-api-executable
  • Google Apps Script リファレンス「Class TextOutput」 https://developers.google.com/apps-script/reference/content/text-output
  • Google Apps Script リファレンス「Class Lock」 https://developers.google.com/apps-script/reference/lock/lock
  • Google Apps Script リファレンス「Class Properties」 https://developers.google.com/apps-script/reference/properties/properties
  • Google Apps Script「Quotas for Google Services」 https://developers.google.com/apps-script/guides/services/quotas

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「GAS WebアプリのdoGet・doPost|デプロイ方法とJSONの返し方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?