Use Cases (更新: 2026/6/7)

API設計の進め方をClaude Codeと一緒に:要件→エンドポイント→スキーマ→エラー

Claude Codeを相棒にAPIを設計する進め方。要件の整理からリソースとエンドポイント、入出力スキーマ、エラー設計、命名とステータスコードの一貫性、設計レビューまでを順番に。

API設計の進め方をClaude Codeと一緒に:要件→エンドポイント→スキーマ→エラー

「APIサクッと作って」とClaude Codeに頼んだら、30分で動くものが出てきました。すごい、と思った3日後、フロントの人から「getUserOrdersorders/list どっちが正です?」と聞かれて固まりました。

同じ機能のエンドポイントが、名前ちがいで2本生えていたんです。片方は 404 を返し、もう片方はエラーでも 200 を返す。利用者からすれば、どっちを信じればいいのか分からない。

このとき気づきました。AIにコードを書かせるのは速い。でも**「何を作るか」を先に決めずに書かせると、速く散らかるだけ**だと。設計の「進め方」そのものを相棒と共有しないと、後から倍の時間で片付けるはめになります。

今日は、僕がいまやっている「Claude Codeと一緒にAPIを設計する順番」を、丸ごと書きます。REST設計の原則そのものや、仕様書化のやり方は別記事に逃がして、ここでは決める順番だけに集中します。

この記事の要点

  • APIは「かっこいいURL」ではなく、利用者との約束。先に決めるのは入力・出力・失敗の3つ。
  • Claude Codeは「コードを書く係」より「設計を一緒に決めてレビューする係」にすると事故が減る。
  • 進め方は固定で5ステップ。①要件を一行ずつ→②リソースとエンドポイント→③入出力スキーマ→④エラー設計→⑤一貫性レビュー。
  • 命名とステータスコードは最初に表で固定してからClaudeに渡す。あとで揃えるのは地獄。
  • 設計レビューは「まだコードは書くな、指摘だけ出せ」と頼むのがコツ。差分が小さく保てる。

なぜ「コードを書かせる」前に「設計を決める」のか

APIは、別のプログラムから使われる「画面のない窓口」です。人間向けの画面ならボタンの位置やラベルで意図が伝わりますが、APIで伝わるのはURL、HTTPメソッド、ステータスコード、JSONの項目名、エラーの形だけ。つまり、その5つがそのまま「説明書」になります。

ここをAIに丸投げすると何が起きるか。Claude Codeはとても素直なので、頼まれた範囲で「それっぽく動くもの」を全力で作ります。けれど、認証は? ページめくりは? 同じ注文を2回送られたら? ——こういう「頼まなかったこと」は、当然ながら抜けます。後から足すと、すでに書かれたコードとの差分が膨らみ、レビューもしづらくなる。

だから順番が逆なんです。先に人間(と相棒)で設計を決めて、それからコードを書かせる。設計が一枚あれば、Claude Codeの出力は「正しいかどうか照らし合わせる対象」になります。設計がないと、出力そのものが唯一の真実になってしまって、誰も間違いに気づけません。

僕が小さな検証プロジェクトで試したときも、最初にURLだけ生成させた案は悪くありませんでした。でも認証・ページネーション・冪等性・エラーの粒度を後から足したら、差分がどんどん大きくなった。最初のひと言に「利用者が失敗したときの戻り方」まで含めておくほうが、結果として短いコードで済みます。

なお、REST設計の原則(リソース中心・メソッド・ステータスの使い分け)そのものを腰を据えて固めたいときはClaude CodeでREST API設計を固める手順を、決まった設計をOpenAPIで仕様書にしてSwagger UIで共有する段階はOpenAPIでの仕様化と型生成を見てください。この記事は、その手前の「どういう順番で決めるか」だけを扱います。

ステップ1:要件を「一行ずつ」整理する

最初にやるのは、コードでも図でもなく、業務ルールを短い箇条書きにすることです。長い仕様書はいりません。むしろ短いほうがいい。

たとえば注文APIなら、こんな感じ。

  • 注文を作れる。作ったら注文IDが返る。
  • 注文の中身(商品と数量)は1件以上。空はダメ。
  • 同じ注文を間違って2回送っても、二重で作られない。
  • 自分の会社の注文だけ見られる。他社の注文は見えない。
  • 失敗したら、利用者が「どこを直せばいいか」分かる形で返す。

この5行を、そのままClaude Codeに渡します。ポイントは、「失敗したとき」と「やってはいけないこと」を最初から書くこと。AIは「うまくいく道(ハッピーパス)」は得意ですが、失敗の道は明示しないと考えてくれません。

claude -p "
これからEC注文APIを設計します。まだコードは書かないでください。
以下の業務ルールから、不足している『決めるべき項目』を質問形式で挙げてください。
- 注文を作れる。作ったら注文IDが返る。
- 注文の中身は1件以上。空はダメ。
- 同じ注文を2回送っても二重で作られない。
- 自分の会社の注文だけ見られる。
- 失敗時は利用者が直せる形でエラーを返す。
"

「コードは書くな、質問だけ出せ」と縛るのがコツです。すると「金額は税込みか税抜きか」「注文のキャンセルはどう表すか」「一覧は何件ずつ返すか」みたいな、自分では抜けていた論点を相棒が拾ってくれます。設計の最初の価値は、コードよりこの質問リストにあります。

ステップ2:リソースとエンドポイントを決める

要件が固まったら、次は「何という名詞を扱い、それに何をするか」です。ここでREST設計の基本を一回だけ思い出します。URLは名詞、操作はHTTPメソッドPOST /orders/create ではなく POST /ordersGET /getOrder?id=1 ではなく GET /orders/1 です。

冒頭の僕の事故は、まさにここで起きました。getUserOrdersorders/list が併存したのは、命名のルールを先に決めずにClaude Codeへ何度も小出しに頼んだからです。だから今は、エンドポイント一覧を先に表で固定してから渡します。

やりたいことメソッドパス補足
注文を作るPOST/v1/orders冪等キー必須
注文を1件見るGET/v1/orders/{id}他社のは404
注文一覧GET/v1/ordersページング前提
注文をキャンセルPOST/v1/orders/{id}/cancellation状態を変える操作
顧客を1件見るGET/v1/customers/{id}

この表をそのままプロンプトに貼ると、Claude Codeは勝手に別名を生やしにくくなります。命名の一貫性は「あとで揃える」のがいちばん高くつくので、最初の一枚で握ってしまうのが正解です。バージョンの /v1 も最初から入れておくと、将来の互換性の話がしやすくなります。

ステップ3:入出力スキーマを決める

エンドポイントが決まったら、次は「各窓口が、どんなJSONを受け取り、どんなJSONを返すか」です。ここで type: string だけで済ませないこと。それは「文字列です」としか言っておらず、長さも形式も決まっていないので、利用者ごとに解釈がぶれます。

僕は、ここでも一度Claudeに「制約まで含めて書いて」と明示します。最小文字数、配列の最小件数、列挙できる値、必須かどうか——このあたりを最初に固めると、後段のテストもバリデーションも安定します。

決めた内容は、OpenAPI 3.1 形式のスキーマにしておくと共有しやすいです。下は注文作成の入力と、返ってくる注文オブジェクトの例。

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /v1/orders:
    post:
      summary: 注文を作成する
      operationId: createOrder
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key   # 同じ注文の二重作成を防ぐ一意キー
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "201":
          description: 作成成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "422":
          description: 入力内容が不正
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerId, items]   # この2つは必須
      properties:
        customerId:
          type: string
          minLength: 3                # 形だけでなく最小長まで決める
        items:
          type: array
          minItems: 1                 # 空の注文は受け付けない
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku:
                type: string
                minLength: 3
              quantity:
                type: integer
                minimum: 1            # 数量は1以上の整数
    Order:
      type: object
      required: [id, status, customerId, total]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [accepted, cancelled] # 取りうる状態を明示
        customerId:
          type: string
        total:
          type: integer               # 金額の単位はチームで先に合意する

このスキーマを書く段階で、自然と「total は税込み? 通貨は?」という会話になります。それでいいんです。スキーマは設計の会話を引き出す道具でもあります。null と「項目を送らない」を混ぜないことも、ここで決めておきます。nickname: null が「消す」なのか「未設定」なのか曖昧だと、クライアントごとに挙動が割れます。

ステップ4:エラー設計を決める(ここで差がつく)

正直に言うと、僕が昔いちばん手を抜いていたのがエラー設計でした。とりあえず全部 200 で返して、本文に {"error": "失敗"} と書く。これが冒頭の事故の片割れです。

なぜダメか。HTTPのステータスコードは、利用者・SDK・監視ツール・ロードバランサーがみんなで読む共通語だからです。業務エラーを 200 に隠すと、リトライすべきか、利用者に謝るべきか、誰も機械的に判断できなくなります。どのコードがどんな意味かは、公式のRFC 9110 HTTP Semanticsが一次情報です。

だから、失敗の意味をコードで分けます。覚えるのは5つだけで十分です。

状況コード意味
JSONが壊れている400そもそも読めない
認証されていない401誰だか分からない
権限がない403誰かは分かるが許可がない
対象が存在しない404そんな注文はない
入力の中身が不正422形式は読めたが内容がダメ

返すエラー本文も「次にどうすればいいか」が分かる形にします。errors 配列に「どの項目が」「なぜダメか」を入れる。下のような最小サーバーで、設計どおりにエラーが返るか実際に確かめられます。Node.jsの標準ライブラリだけで動くので、外部依存ゼロです。

import { createServer } from "node:http";
import { randomUUID } from "node:crypto";

// JSONボディを安全に読む(巨大ボディは拒否)
function readJson(req) {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk;
      if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
    });
    req.on("end", () => {
      if (!body) return resolve({});
      try { resolve(JSON.parse(body)); } catch (error) { reject(error); }
    });
    req.on("error", reject);
  });
}

function send(res, status, body) {
  res.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "x-content-type-options": "nosniff",
  });
  res.end(JSON.stringify(body, null, 2));
}

// 「次に何をすればいいか」を返すエラー本文
function problem(status, title, detail, errors = []) {
  return { type: "about:blank", title, status, detail, errors };
}

// 入力の中身を検証して、ダメな箇所を全部集める
function validateOrder(input) {
  const errors = [];
  if (typeof input.customerId !== "string" || input.customerId.length < 3) {
    errors.push({ path: "customerId", message: "customerIdは3文字以上の文字列にしてください" });
  }
  if (!Array.isArray(input.items) || input.items.length === 0) {
    errors.push({ path: "items", message: "itemsは1件以上必要です" });
  }
  for (const [i, item] of (input.items ?? []).entries()) {
    if (!Number.isInteger(item?.quantity) || item.quantity < 1) {
      errors.push({ path: `items.${i}.quantity`, message: "quantityは1以上の整数にしてください" });
    }
  }
  return errors;
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");

  if (req.method === "GET" && url.pathname === "/health") {
    return send(res, 200, { ok: true });
  }

  if (req.method === "POST" && url.pathname === "/v1/orders") {
    // 冪等キーがなければ400(二重作成を防ぐ約束を強制)
    if (!req.headers["idempotency-key"]) {
      return send(res, 400, problem(400, "Idempotency-Keyがありません", "POST /v1/orders にはヘッダが必要です"));
    }
    try {
      const body = await readJson(req);
      const errors = validateOrder(body);
      if (errors.length > 0) {
        return send(res, 422, problem(422, "入力内容が不正です", "errorsの項目を直してください", errors));
      }
      return send(res, 201, {
        id: `ord_${randomUUID()}`,
        status: "accepted",
        customerId: body.customerId,
        total: 4200,
      });
    } catch {
      return send(res, 400, problem(400, "JSONが壊れています", "リクエストボディはJSONにしてください"));
    }
  }

  // どのルートにも当たらなければ404
  return send(res, 404, problem(404, "見つかりません", `${req.method} ${url.pathname} は未定義です`));
});

server.listen(3000, () => console.log("Mock API: http://localhost:3000"));

起動して、3パターン叩いてみます。

node mock-server.mjs
# 正常 → 201
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-001" \
  -d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'

# 中身が空 → 422(errors配列に理由が入る)
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-002" \
  -d '{"customerId":"x","items":[]}'

# 冪等キーなし → 400
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'

このモックは「設計の試し打ち」です。コードを清書する前に、決めたステータスコードとエラー形が利用者目線で納得できるかを、手で触って確認できます。

ステップ5:一貫性をClaude Codeにレビューさせる

設計の最後は、相棒に「粗探し」をさせる工程です。ここがClaude Codeの本領で、人間が見落としがちな不整合を拾わせます。コツは前と同じ、「まだコードは直すな、指摘だけ重大度順に出せ」と縛ること。

claude -p "
docs/openapi.yaml を設計レビューとして読んでください。まだ編集はしないでください。
Findings を重大度順に挙げてください。観点は次のとおり:
- 命名の一貫性(複数形・動詞の混入・似た意味の別名がないか)
- HTTPメソッドとステータスコードがRFC 9110の意味に合っているか
- 入力スキーマに制約(必須・最小長・列挙)が抜けていないか
- 認証が必要なのに security が付いていない操作がないか
- 他人のリソースを見られてしまう設計になっていないか
"

なぜ「指摘だけ」にするか。いきなり修正までさせると、何をどう変えたのか差分が大きくなって、人間がレビューできなくなるからです。指摘→自分で取捨選択→必要な分だけ直させる。この往復にすると、Claude Codeの出力を鵜呑みにせず、設計の主導権を自分で握れます。

レビュー観点で特に効くのが「命名の一貫性」と「認可の抜け」です。前者は冒頭の僕の事故そのもの。後者は、Bearer token で認証していても /v1/orders/{id} で他人の注文が見えてしまう、というよくある穴です。認証(誰か)と認可(その人が見ていいか)は別物で、後者はAIも人間も忘れがち。だからレビュー項目に毎回入れています。

僕がやらかした設計の失敗3つ

正直に書きます。最初のころのAPI設計は事故だらけでした。

ひとつ目は、命名を後回しにしたこと。冒頭のとおり、同じ機能のエンドポイントが別名で2本生えました。原因は、設計を表で固定する前にClaude Codeへ小出しに頼んだこと。いまは必ずエンドポイント表を一枚作ってから渡します。

ふたつ目は、全部 200 で返したこと。業務エラーをボディの中に隠したせいで、フロントのリトライ処理が暴走しました。422400 を分けるだけで、利用者側の実装が一気に素直になりました。

みっつ目は、レスポンス項目を軽い気持ちで消したこと。サーバー側では小さな整理のつもりでも、古いモバイルアプリにとっては本番クラッシュです。項目を消すのは破壊的変更。いまは消す前に、非推奨期間と移行先を必ず決めます。互換性をどう運用するかはAPIバージョニング戦略に寄せています。

よくある質問

Q. 結局、Claude Codeに最初から「全部設計して」と頼んじゃダメなんですか? A. ダメではないですが、おすすめしません。一度に全部出させると、命名や認可の抜けが混ざったまま大きな成果物が出てきて、レビューが追いつきません。要件→エンドポイント→スキーマ→エラー→レビューと小分けにするほうが、結果的に速いです。

Q. RESTじゃなくてGraphQLやgRPCでも同じ進め方でいいですか? A. 「先に入力・出力・失敗を決める」という骨格は同じです。違うのはステップ2の表現方法(リソース表の代わりにスキーマや.protoになる)だけ。この記事のステップ1・3・4・5はそのまま使えます。

Q. ステータスコードは何種類覚えればいいですか? A. まずは 400 / 401 / 403 / 404 / 422 の5つと、成功側の 200 / 201 で十分です。迷ったら「形が読めないなら400、形は読めたが中身がダメなら422」と覚えてください。

Q. 設計をOpenAPIで書くのと、コードを先に書いてから仕様を起こすの、どっちがいい? A. チームで使うAPIなら設計(OpenAPI)が先です。仕様が先にあると、フロントとバックが同時に動けます。書き方とSwagger UIでの共有はOpenAPIでの仕様化と型生成の記事にまとめています。

Q. Claude Codeのレビューはどこまで信用していい? A. 「指摘の一覧」としては優秀ですが、最終判断は人間です。特に業務ルール(金額の税込み判定など)は、AIは文脈を知らないので外します。指摘を叩き台に、自分で取捨選択してください。

実際に試した結果

この進め方に切り替えてから、冒頭のような「別名エンドポイント事故」はゼロになりました。理由ははっきりしていて、ステップ2でエンドポイント表を一枚に固定してからClaude Codeへ渡すようにしたからです。

この記事のモックサーバーも、Node v24系で起動して、GET /health200、正常な POST /v1/orders201、空の items422、冪等キーなしが 400 を返すことを手元で確認しました。コードを書く速さより、「何を作るかを先に決めて、相棒にレビューさせる」順番のほうが、結局いちばん早く崩れないAPIにたどり着く——これが、何度か事故ったあとの僕の結論です。

設計レビューやOpenAPI整備をチームに入れたい段階なら研修・相談、まず自分で型を身につけたいなら教材一覧から始めるのが自然な流れだと思います。

#claude-code #api設計 #rest #エンドポイント設計 #命名
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。