AI コーディングで本番リリースする前にノンデベロッパーにも押さえてほしい 15 の観点
Claude Code や Codex、Cursor などの 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);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");12. 認証まわりのデフォルトを外していないか — Auth Defaults
セキュリティ系は自作しないのが鉄則で、AI に生成させたコードでも、認証まわりだけは既存のライブラリ(Auth.js、Clerk、Better 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 の観点を整理した、現場からお送りしました。