- クラウドサービス
GAS WebアプリのdoGet・doPost|デプロイ方法とJSONの返し方
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つとしています。
- doGet(e) または doPost(e) という関数がある。
- その関数が、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 がありません"}} |
デプロイの手順
公式ドキュメントの手順は次のとおりです(項目名はドキュメントの英語表記。画面の表示言語が日本語のときは、対応する項目を選びます)。
- スクリプト エディタの右上で Deploy → New deployment をクリックする。
- 「Select type」の横にある Enable deployment types(設定のアイコン)から Web app を選ぶ。
- 「Deployment configuration」に、実行ユーザー(Execute as)やアクセスできるユーザーなどを入力する。
- 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」の次の手順で、同じデプロイを新しいバージョンに更新します。
- Deploy → Manage deployments を開く。
- 対象のデプロイを選び、Edit をクリックする。
- バージョンで新しいバージョンを作成して選ぶ。
- 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期連続の黒字決算。