AI コーディングで本番リリースする前にノンデベロッパーにも押さえてほしい 15 の観点

重岡 正 · Mon, July 13, 2026

Claude CodeCodexCursor などの AI コーディングエージェントを使えば、コードが読めるノンデベロッパーでも動く MVP や社内ツールを 1 日で立ち上げられます。ここまでは既知の話です。問題はその先で、「動いたっぽいから公開する」と、ユーザーが二重課金される、深夜にサーバーが落ちる、監査ログが嘘をつく、といった事故が起きやすくなります。

これは AI がサボっているからではありません。AI は「言われた仕様を満たすコード」を書くのは得意ですが、「言われていない本番運用上の配慮」は構造上こぼしがちです。逆に言えば、こぼれる場所は毎回だいたい同じで、パターン化できます。本記事はそのパターンを 15 項目に整理したチェックリストです。

この記事で得られること

  • AI 生成コードがどこで本番運用に耐えられなくなるか、原因と例が分かる
  • ノンデベロッパーでも実行できる「本番投入前の 15 項目チェック」の観点が手に入る
  • 各項目について、AI がよく出す Bad コード例と、その修正の勘所を確認できる
  • 事故のシナリオ(二重課金、白い画面、個人情報漏れなど)を具体的にイメージできる
  • 最後に、このチェックリスト自体を AI に投げてセルフレビューさせるプロンプト例が手に入る

1. 壊れたときに「白い画面」にしないか — Graceful Degradation

「Graceful Degradation(=一部が壊れても全体は使える状態を保つこと)」は、本番運用の基礎です。AI は正常系の見た目のよいコードを書くのが得意な一方、「もし例外が投げられたら」の受け皿は忘れがちです。

Bad の典型はこれです。

export default function Page() {
  const user = getUser();
  return <Profile name={user.name} />;
}

getUser() が API 障害で null を返した瞬間、user.name で例外が起きて画面全体が真っ白になります。ユーザーから見ると「サービスが死んだ」と区別がつきません。

  • Check: 各ページ / ルートに、エラー時のフォールバック UI が用意されているか(React なら Error Boundary、Next.js なら error.tsx

2. 同じ操作を 2 回押しても大丈夫か — Idempotency

「冪等性(べきとうせい、=同じ操作を何度やっても結果が変わらない性質)」は、決済・登録・通知など「1 回だけやりたい」処理では必須です。ネットワークが不安定なとき、リトライを組み込んだクライアントライブラリや SDK は自動で再送します。サーバー側で「これは前と同じリクエストだ」と判定できないと、二重登録・二重課金・監査ログの二重記録が発生します。

// Bad: 同じリクエストが 2 回来たら 2 回課金される
app.post("/charge", async (req, res) => {
  await stripe.charges.create(req.body);
  res.json({ ok: true });
});

事故シナリオは分かりやすくて、ユーザーが「反応がないからもう 1 回」ボタンを押すと 2 回課金され、翌朝カスタマーサポートに問い合わせが来ます。

  • Check: 課金・作成・通知系の POST に「同一リクエスト ID(Idempotency-Key)」で重複を弾く仕組みが入っているか

3. 外部 API 呼び出しに「諦める時間」があるか — Timeout

外部 API(OpenAI や決済プロバイダ、メール送信サービスなど)は、たまに応答が返ってこないことがあります。AI が書いたコードはタイムアウト指定を忘れやすく、fetch(url) とだけ書かれていると理論上は無限に待ちます。1 リクエストがハングするだけならまだしも、同じサーバープロセスに来た他のユーザーのリクエストまで巻き添えを食います。

// Bad
const res = await fetch("https://api.example.com/generate");
 
// Good: 諦める時間を決める
const res = await fetch("https://api.example.com/generate", {
  signal: AbortSignal.timeout(10_000),
});
  • Check: すべての外部呼び出しに timeout / AbortSignal が付いているか

4. ヘルスチェックが「本当に生きてるか」を見ているか — Health Check

ロードバランサや Kubernetes は、ヘルスチェック用のエンドポイントを定期的に叩いて、返事の悪いサーバーを外そうとします。AI はこれを頼まれると、たいてい次のように書きます。

// Bad
app.get("/health", (_, res) => res.json({ status: "ok" }));

これは「Node.js のプロセスが生きているか」しか見ていません。DB が落ちていても、Redis が落ちていても 200 を返すので、ロードバランサは壊れたサーバーに延々とリクエストを流し続けます。障害の全体像が見えなくなる、いちばん厄介な種類の壊れ方です。

  • Check: /health が DB や重要な外部依存に 1 回 ping してから ok を返しているか

5. 入力を必ず疑う — 境界バリデーション

TypeScript の型は、コンパイル時にしか効きません。実行時にサーバーへ入ってくる値は、型がついていても嘘の可能性があります。守るべき境界は 5 つあります。HTTP のリクエストボディ、URL パラメータ、環境変数、DB から読んだ値、外部 API の返却値です。

// Bad
app.post("/user", (req, res) => {
  db.insert(req.body); // 何が入ってくるか分からない
});
 
// Good: zod などで実行時検証
const schema = z.object({ email: z.string().email() });
const parsed = schema.parse(req.body);
  • Check: zodvalibot など実行時バリデーションが、外部との境界すべてに掛かっているか

6. 一覧 API に上限があるか — Unbounded Query

「ユーザー一覧を返す API」は開発中の 10 人のときは軽快に動きますが、本番で 10 万人に育ったとき、突然サーバーのメモリを食い潰します。

// Bad
app.get("/users", async (_, res) => {
  const users = await db.select().from(usersTable); // 全件取得
  res.json(users);
});

事故シナリオは、営業がスプレッドシートに貼り付けようとして CSV エクスポートを叩き、サーバーが 30 秒後にメモリ不足で落ちる、みたいな話です。

  • Check: リスト系 API に必ず最大件数(例:200 件)の clamp(=上限で切り詰め)とページング(cursor / offset)が入っているか

7. 同時編集の衝突を想定しているか — Race Condition

管理画面で 2 人が同時に同じユーザーを編集する、といったケースを AI は考慮しません。単純に「取得して変更して保存する」だけを書きます。

// Bad
const user = await getUser(id);
user.role = "admin";
await saveUser(user); // 後勝ちで、もう 1 人の変更が消える

このパターンは監査ログとの相性も悪くて、「変更前は viewer だった」というログが後で見返すと嘘になります。特に権限変更のような、事後に説明責任を問われる操作で刺さります。

  • Check: 重要なエンティティに version 列や updated_at を持たせ、If-Match ヘッダ / 楽観ロックで衝突を検出しているか

8. エラーメッセージから内部が漏れていないか — Information Disclosure

例外を握らずにそのままレスポンスへ流すと、スタックトレースや SQL 文が JSON に混ざってクライアントへ返ります。DB のテーブル名、内部パス、使っている ORM のバージョンまで攻撃者に読まれます。

// Bad
} catch (e) {
  res.status(500).json({ error: e.stack });
}
  • Check: 本番環境では汎用的なメッセージ(“internal error”)だけを返し、詳細はサーバーログ側にだけ書く分離ができているか

9. 個人情報を URL に載せていないか — PII in URL

GET /reset?email=user@example.com&token=xxxx のような URL は、CDN のアクセスログ、ブラウザ履歴、Referer ヘッダ経由の外部サイトの解析ログに、平文でずっと残り続けます。AI は「動くから」でクエリに何でも詰めがちです。

  • Check: URL クエリに email / 電話番号 / トークン / 生年月日などが乗っていないか。載せるべきは POST ボディや Authorization ヘッダ

10. リトライ可能なエラーとそうでないエラーが区別されているか — Retry Semantics

上流 API が 429(Too Many Requests、レート制限に達した)を返してきたとき、AI が書いたコードは往々にしてこう変換します。

// Bad: すべての失敗を 400 にまとめる
} catch (e) {
  res.status(400).json({ error: "failed" });
}

400 は「クライアントの入力が悪い、再試行しても無駄」の意味です。本当は「少し待ってから再試行して」だったものを 400 に潰すと、クライアントの再試行戦略が全部壊れます。

  • Check: HTTP ステータスと Retry-After ヘッダを保持したままクライアントへ伝えているか

11. ログに「誰の・どのリクエストか」が入っているか — Structured Logging

console.error("failed") だけのログは、本番障害の犯人特定に使えません。10 万件のリクエストのうち、どのユーザーの、どのエンドポイントで失敗したか分からないからです。

// Good: 構造化ログ
logger.error({
  requestId: req.id,
  userId: req.user?.id,
  route: "/api/charge",
  err,
}, "charge failed");
  • Check: 全ログに requestId / userId / ルート名が構造化された形(JSON など)で入っているか。pinowinston のような構造化ロガーを使っているか

12. 認証まわりのデフォルトを外していないか — Auth Defaults

セキュリティ系は自作しないのが鉄則で、AI に生成させたコードでも、認証まわりだけは既存のライブラリ(Auth.jsClerkBetter Auth など)のデフォルトに従うのが安全です。特に注意したい定番は以下です。

  • Cookie に HttpOnly / Secure / SameSite が付いているか

  • パスワードリセットやログイン API にレート制限が入っているか

  • アクセストークン / セッショントークンを localStorage に保存していないか(XSS 一発で全部抜かれます)

  • Check: 認証系はライブラリのデフォルト設定を意図せず緩めていないか

13. アクセシビリティの最低限 — A11y

社内ツールでも「気の利き」ではなく「機能」としての A11y(=キーボード操作やスクリーンリーダーでの利用可能性)が求められます。AI は視覚的に完成しているコードを出すため、A11y は落ちがちです。

  • ページタイトル (<title>) がルートごとに変わるか

  • キーボードだけで全操作できるか(Tab 順序、フォーカスリング、スキップリンク)

  • アイコンだけのボタンに aria-label が付いているか

  • 色だけで状態を伝えていないか(赤 / 緑だけの色覚差異)

  • Check: axe DevTools など自動チェッカーを 1 回でも走らせたか

14. URL に画面の状態が載っているか — URL State

フィルタ、検索クエリ、ページ番号、ソート順が「リロードすると消える」画面を、AI は量産します。React の useState にすべて閉じ込めるためです。ユーザーとしては、絞り込んだ画面の URL を同僚に送っても再現できない、ブラウザバックで戻ると絞り込みがリセットされている、という不便が積み重なります。

  • Check: 共有 URL で同じ画面が再現できるか。URL クエリや Next.js の searchParams で状態を持てているか

15. AI に「レビュー観点リスト」を渡してセルフレビューさせる — Self-Review

ここまでの 14 項目(この項番 15 自体を除く)に共通するのは、AI は「言われたことを書くのは得意」だが「言われていない配慮は苦手」ということです。ここから導ける最も費用対効果の高い手は、単純で、上記のチェックリスト自体を AI に読ませて、自分のコードをレビューさせることです。

たとえば Claude Code や Codex、Cursor のチャットに、以下のようなプロンプトを投げます。

以下は「本番投入前のレビュー観点」です。
これから提示する変更内容を、この 14 項目それぞれについて
「該当なし / 対応済み / 要修正」で採点し、
要修正のものだけコード修正案を出してください。
主観的な感想は不要、コードとしての事実だけを述べてください。
 
# レビュー観点
1. Graceful Degradation(エラー時のフォールバック UI があるか)
2. Idempotency(POST 系に Idempotency-Key があるか)
3. Timeout(外部呼び出しに AbortSignal があるか)
4. ヘルスチェック(DB を ping しているか)
5. 境界バリデーション(zod など実行時検証があるか)
6. Unbounded Query(LIMIT / clamp があるか)
7. Race Condition(version / If-Match での楽観ロックか)
8. エラーメッセージからの内部漏洩
9. 個人情報を URL に載せていないか
10. リトライ可能エラーの区別(429 / Retry-After)
11. 構造化ログ(requestId / userId が入っているか)
12. 認証のデフォルト(HttpOnly / SameSite / localStorage)
13. A11y(title / aria-label / キーボード)
14. URL への状態反映
 
# 変更内容
(ここに diff を貼る)

これを最初のレビューアーとして走らせてから人間の目で最終確認する、という運用に切り替えるだけで、事故率がかなり下がります。AI は 100 点を目指すレビューアーとしては信用できませんが、「頭出し」と「見落としの網羅」だけであれば十分に信用できます。

締めに — AI コーディング時代に人間が持ち続ける役割

AI コーディングは「書く速度」は無限に速くなりました。しかし「設計の勘所」は、当面のあいだ人間の側に残ります。ノンデベロッパーがやるべきなのは「全部自分で書けるようになる」ことではなく、「危ないパターンの匂いを嗅ぎ分けて、AI に “そこ違うよ” と言えるようになる」ことです。そのための最短距離が、このチェックリストを手元に置いて、AI にも同じチェックリストを共有しておくことです。

本番投入前の 15 項目チェックリスト(再掲)

  • 1 Graceful Degradation:ページごとにエラー時のフォールバック UI があるか
  • 2 Idempotency:課金・作成系 POST に Idempotency-Key があるか
  • 3 Timeout:すべての外部呼び出しに AbortSignal / timeout があるか
  • 4 ヘルスチェック:DB や依存を 1 回 ping してから ok を返しているか
  • 5 境界バリデーション:外部からの入力に zod など実行時検証があるか
  • 6 Unbounded Query:リスト系 API に最大件数 clamp とページングがあるか
  • 7 Race Condition:version / If-Match で楽観ロックしているか
  • 8 エラーメッセージ:本番では詳細をレスポンスに返していないか
  • 9 URL に PII:email / 電話番号 / トークンを URL に載せていないか
  • 10 リトライ可能エラー:429 / Retry-After を握り潰していないか
  • 11 構造化ログ:requestId / userId / ルート名が入っているか
  • 12 認証デフォルト:HttpOnly / Secure / SameSite / localStorage NG
  • 13 A11y:title / aria-label / キーボード操作
  • 14 URL に状態:共有 URL で同じ画面が再現できるか
  • 15 セルフレビュー:AI にこのチェックリストで自分の diff を採点させる

以上、AI が書いたコードを本番投入する前にノンデベロッパーが見るべき 15 の観点を整理した、現場からお送りしました。