APIテストをsupertest+Vitestで自動化する。Claude Codeに「200だけ見るな」と言う話
Claude CodeのAPIテストが200 OKしか見ない問題を、supertestとfetch+Vitestで解決。認証込みの検証、契約テスト、テストDBとモック、CI実行まで失敗談つきで。
「ログインAPIのテスト書いといて」と頼んだら、Claude Codeはこんなテストを返してきました。
expect(res.status).toBe(200);
一行。緑になる。確かにテストは通る。でも本番では、そのログインAPIがレスポンスに password を丸ごと含めて返していました。200は返っていたんです。形が壊れていただけで。
僕はこのとき、APIテストの本当の敵は「失敗するテスト」じゃなくて「何も守っていないのに緑のテスト」だと知りました。今日はその緑の安心を、本物の安心に変える話をします。
この記事の要点
- APIテストは単体テストとE2Eの「ちょうど中間」。サーバーの入出力だけをHTTPで直接叩くので、速くて原因の切り分けが早い
- 緑にするだけのテストは無価値。見るのは「ステータス・JSONの形・認証・異常系・契約・テストデータ・CI」の7点
- Expressなら
supertest、フレームワーク非依存ならfetch+ Vitestが手早い。両方そのまま動くコードを置いた - Claude Codeには「正常系だけ書くな、認証なし・入力不正・存在しないIDも入れろ」と先に縛りを渡すと薄いテストにならない
- 単体テストの磨き込みはVitestのモックとカバレッジ、画面ごとのE2EはPlaywright E2Eへ。APIテストはその真ん中を埋める層
APIテストは単体とE2Eの「中間」にいる
テストの層を、配達でたとえます。
単体テストは「この計算関数、足し算合ってる?」を見る、部品レベルの確認。E2Eテストは「お客さんがブラウザで注文ボタンを押して、注文完了画面が出るか」を見る、最初から最後まで通しの確認。
APIテストはその真ん中です。画面は開かない。でも本物のHTTPリクエストをサーバーに送って、「POST /orders を叩いたら201が返って、注文IDが入ったJSONが来るか」を見る。配達でいえば、玄関のチャイムは鳴らさないけど、荷物がトラックから正しく出てくるかを荷台の前で確認する感じです。
なぜこの層が要るのか。E2Eは価値が高いけど、画面・ブラウザ・認証・ネットワーク・DBが全部動くので、落ちたときに「どこが原因か」を探すのが遅い。単体テストはHTTPの世界を見ない。APIテストは画面を飛ばして、サーバーの約束だけを短時間で確かめられる。だから「壊れてるかどうかを一番早く知れる」場所なんです。
この記事はその真ん中の層に集中します。関数の単体テストを落ちない状態にする話はVitestのモックとカバレッジに、画面ごとのフレーキーを潰す話はPlaywright E2Eに分けてあるので、住み分けて読んでください。
緑のテストが守っていない7つの観点
冒頭の password 事故が起きた理由ははっきりしています。status しか見ていなかった。APIテストで本当に見るべきは、次の7点です。
| 観点 | やさしい言い換え | 具体例 |
|---|---|---|
| スモークテスト | 最低限の生存確認 | /health が204、ログインが200 |
| ステータスコード | 結果を示す数字の合図 | 作成は201、認証なしは401、未存在は404 |
| JSONの形 | 必須キーと返してはいけないキー | sessionId は文字列、password は返さない |
| 認証 | 誰として呼んでいるか | Bearerトークン、Cookie、APIキー |
| 異常系 | わざと失敗する入力を送る | パスワード違い、空の注文、署名なしWebhook |
| 契約テスト | 仕様と実装のズレ確認 | OpenAPIの必須フィールドと実レスポンスを比べる |
| テストデータ | 毎回同じ条件で始める仕組み | ローカルDB、モック、使い捨ての注文ID |
ポイントは、200が返っても安心しないこと。200なのに金額フィールドが消えていたり、エラー形式が画面側の想定と違っていたり、認証なしでも注文が作れてしまったら、品質保証としては全部アウトです。緑は「壊れていない証拠」ではなく「ここまでは確認した印」でしかありません。
supertestで認証込みのエンドポイントを叩く
Expressのようなフレームワークを使っているなら、supertest が一番手早い。サーバーをポートにbindせず、アプリのインスタンスを直接渡してHTTPリクエストを再現してくれるからです。テストランナーはVitestを使います。
まず入れます。
npm install -D vitest supertest
npm install express
アプリ本体とテストを分けて書きます。ここがコツで、app を listen せずに export しておくと、テストから何度でも安全に叩けます。
// app.mjs
import express from "express";
export function createApp() {
const app = express();
app.use(express.json());
const sessions = new Map();
const orders = new Map();
let orderSeq = 1;
// ログイン: 成功でセッションIDを返す。パスワードは絶対に返さない
app.post("/login", (req, res) => {
const { email, password } = req.body ?? {};
if (email !== "[email protected]" || password !== "correct-horse") {
return res
.status(401)
.json({ error: { code: "invalid_credentials", message: "認証情報が違います" } });
}
const sessionId = `sess_${orderSeq}_${Date.now()}`;
sessions.set(sessionId, { id: "user_1", email });
return res.status(200).json({ sessionId, user: { id: "user_1", email } });
});
// 認証ミドルウェア: Bearerトークンが無ければ401で止める門番
const auth = (req, res, next) => {
const header = req.headers.authorization ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
const user = sessions.get(token);
if (!user) {
return res
.status(401)
.json({ error: { code: "unauthorized", message: "Bearerトークンが必要です" } });
}
req.user = user;
next();
};
// 注文作成: 入力を検証し、201と Location ヘッダーを返す
app.post("/orders", auth, (req, res) => {
const items = req.body?.items;
if (!Array.isArray(items) || items.length === 0) {
return res
.status(400)
.json({ error: { code: "validation_failed", message: "itemsは1件以上必要です" } });
}
const totalCents = items.reduce((sum, it) => sum + it.quantity * it.priceCents, 0);
const id = `ord_${orderSeq++}`;
orders.set(id, { id, userId: req.user.id, status: "created", totalCents, items });
return res.status(201).location(`/orders/${id}`).json({ order: orders.get(id) });
});
return app;
}
そしてテスト。supertest に app を渡し、ログインで取ったトークンを次のリクエストに引き継ぎます。認証を「ログイン→トークン取得→そのトークンで叩く」という一連で書くのがAPIテストの肝です。
// orders.test.mjs
import { describe, it, expect, beforeEach } from "vitest";
import request from "supertest";
import { createApp } from "./app.mjs";
let app;
beforeEach(() => {
app = createApp(); // テストごとに新品のアプリ。状態が混ざらない
});
async function loginAndGetToken() {
const res = await request(app)
.post("/login")
.send({ email: "[email protected]", password: "correct-horse" });
expect(res.status).toBe(200);
return res.body.sessionId;
}
describe("認証とJSONの形", () => {
it("ログイン成功でセッションを返し、パスワードは含めない", async () => {
const res = await request(app)
.post("/login")
.send({ email: "[email protected]", password: "correct-horse" });
expect(res.status).toBe(200);
expect(typeof res.body.sessionId).toBe("string");
expect(res.body.user.email).toBe("[email protected]");
// 冒頭の事故を二度と起こさないための一行
expect(res.body.user.password).toBeUndefined();
});
it("パスワードが違えば401と決まったエラーコードを返す", async () => {
const res = await request(app)
.post("/login")
.send({ email: "[email protected]", password: "wrong" });
expect(res.status).toBe(401);
expect(res.body.error.code).toBe("invalid_credentials");
});
});
describe("注文作成API", () => {
it("認証ありで201・Locationヘッダー・合計金額を返す", async () => {
const token = await loginAndGetToken();
const res = await request(app)
.post("/orders")
.set("Authorization", `Bearer ${token}`)
.send({
items: [
{ sku: "book", quantity: 2, priceCents: 1500 },
{ sku: "video", quantity: 1, priceCents: 4000 },
],
});
expect(res.status).toBe(201);
expect(res.headers.location).toMatch(/^\/orders\/ord_/);
expect(res.body.order.totalCents).toBe(7000); // 2*1500 + 4000
});
it("認証なしなら注文を作らせず401で弾く", async () => {
const res = await request(app)
.post("/orders")
.send({ items: [{ sku: "book", quantity: 1, priceCents: 1500 }] });
expect(res.status).toBe(401);
});
it("空の注文は400で止める", async () => {
const token = await loginAndGetToken();
const res = await request(app)
.post("/orders")
.set("Authorization", `Bearer ${token}`)
.send({ items: [] });
expect(res.status).toBe(400);
expect(res.body.error.code).toBe("validation_failed");
});
});
実行はこれだけ。
npx vitest run
正常系だけでなく「認証なしで弾けるか」「空入力を止めるか」「パスワードを返していないか」まで一気に見ています。冒頭の expect(res.status).toBe(200) 一行と比べて、守っている範囲がまるで違うのが分かるはずです。
フレームワークに縛られたくない時のfetch版
supertestはExpress前提です。Fastify・Hono・別言語のサーバー、あるいは「もう動いているサーバーをそのまま叩きたい」なら、Node 18以降に標準で入っている fetch だけで書けます。外部サービスにも本番DBにも一切つながない、自己完結の例を置きます。1ファイルでHTTPサーバーを立てて、ログイン・注文・Webhook・異常系まで検証します。
ファイル名は api-smoke.test.mjs にしてください。
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { createServer } from "node:http";
const TEST_USER = { id: "user_1", email: "[email protected]", password: "correct-horse" };
const WEBHOOK_SECRET = "whsec_test";
function sendJson(res, status, body, headers = {}) {
if (status === 204) {
res.writeHead(status, headers);
res.end();
return;
}
res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
res.end(JSON.stringify(body));
}
function readJson(req) {
return new Promise((resolve, reject) => {
let raw = "";
req.on("data", (chunk) => {
raw += chunk;
if (raw.length > 1_000_000) req.destroy();
});
req.on("end", () => {
if (!raw) return resolve({});
try {
resolve(JSON.parse(raw));
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function bearerToken(req) {
const value = req.headers.authorization;
return typeof value === "string" && value.startsWith("Bearer ") ? value.slice(7) : "";
}
function makeApp() {
const sessions = new Map();
const orders = new Map();
const webhookEvents = new Set();
let orderSeq = 1;
return async function handler(req, res) {
const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", "http://localhost");
let body = {};
if (["POST", "PUT", "PATCH"].includes(method)) {
try {
body = await readJson(req);
} catch {
return sendJson(res, 400, { error: { code: "invalid_json", message: "JSONが不正です" } });
}
}
const currentUser = () => {
const token = bearerToken(req);
return token ? sessions.get(token) : undefined;
};
if (method === "GET" && url.pathname === "/health") return sendJson(res, 204, null);
if (method === "POST" && url.pathname === "/login") {
if (body.email !== TEST_USER.email || body.password !== TEST_USER.password) {
return sendJson(res, 401, { error: { code: "invalid_credentials", message: "認証情報が違います" } });
}
const sessionId = `sess_${randomUUID()}`;
sessions.set(sessionId, { id: TEST_USER.id, email: TEST_USER.email });
return sendJson(res, 200, { sessionId, user: { id: TEST_USER.id, email: TEST_USER.email } });
}
if (method === "POST" && url.pathname === "/orders") {
const user = currentUser();
if (!user) return sendJson(res, 401, { error: { code: "unauthorized", message: "Bearerトークンが必要です" } });
const items = body.items;
if (!Array.isArray(items) || items.length === 0) {
return sendJson(res, 400, { error: { code: "validation_failed", message: "itemsは1件以上必要です" } });
}
const totalCents = items.reduce((sum, it) => sum + it.quantity * it.priceCents, 0);
const order = { id: `ord_${orderSeq++}`, userId: user.id, status: "created", totalCents, items };
orders.set(order.id, order);
return sendJson(res, 201, { order }, { location: `/orders/${order.id}` });
}
if (method === "POST" && url.pathname === "/webhooks/payment") {
if (req.headers["x-webhook-secret"] !== WEBHOOK_SECRET) {
return sendJson(res, 401, { error: { code: "bad_signature", message: "署名が不正です" } });
}
if (typeof body.eventId !== "string" || typeof body.orderId !== "string") {
return sendJson(res, 400, { error: { code: "validation_failed", message: "eventIdとorderIdが必要です" } });
}
if (webhookEvents.has(body.eventId)) return sendJson(res, 200, { received: true, duplicate: true });
const order = orders.get(body.orderId);
if (!order) return sendJson(res, 404, { error: { code: "order_not_found", message: "注文が見つかりません" } });
webhookEvents.add(body.eventId);
order.status = "paid";
return sendJson(res, 202, { received: true, duplicate: false });
}
return sendJson(res, 404, { error: { code: "route_not_found", message: `${method} ${url.pathname} は未対応です` } });
};
}
async function withServer(fn) {
const server = createServer(makeApp());
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
await fn(baseUrl);
} finally {
await new Promise((resolve) => server.close(resolve));
}
}
async function requestJson(baseUrl, path, options = {}) {
const headers = { ...(options.headers ?? {}) };
if (options.token) headers.authorization = `Bearer ${options.token}`;
const init = { method: options.method ?? "GET", headers };
if (options.body !== undefined) {
headers["content-type"] = "application/json";
init.body = JSON.stringify(options.body);
}
const res = await fetch(`${baseUrl}${path}`, init);
const text = await res.text();
return { res, json: text ? JSON.parse(text) : null };
}
async function login(baseUrl) {
const { res, json } = await requestJson(baseUrl, "/login", {
method: "POST",
body: { email: TEST_USER.email, password: TEST_USER.password },
});
assert.equal(res.status, 200);
return json.sessionId;
}
const tests = [];
const test = (name, fn) => tests.push({ name, fn });
test("ログインのスモーク: 200・形・パスワード非返却", async (baseUrl) => {
const health = await fetch(`${baseUrl}/health`);
assert.equal(health.status, 204);
const { res, json } = await requestJson(baseUrl, "/login", {
method: "POST",
body: { email: TEST_USER.email, password: TEST_USER.password },
});
assert.equal(res.status, 200);
assert.equal(typeof json.sessionId, "string");
assert.equal(json.user.email, TEST_USER.email);
assert.equal(json.user.password, undefined);
});
test("注文作成: 201・Location・合計金額", async (baseUrl) => {
const token = await login(baseUrl);
const { res, json } = await requestJson(baseUrl, "/orders", {
method: "POST",
token,
body: { items: [{ sku: "book", quantity: 2, priceCents: 1500 }, { sku: "video", quantity: 1, priceCents: 4000 }] },
});
assert.equal(res.status, 201);
assert.match(res.headers.get("location"), /^\/orders\/ord_/);
assert.equal(json.order.totalCents, 7000);
});
test("Webhook: 署名なし401・重複は安全に処理", async (baseUrl) => {
const token = await login(baseUrl);
const created = await requestJson(baseUrl, "/orders", {
method: "POST",
token,
body: { items: [{ sku: "book", quantity: 1, priceCents: 1500 }] },
});
const orderId = created.json.order.id;
const noSig = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
body: { eventId: "evt_1", orderId },
});
assert.equal(noSig.res.status, 401);
const ok = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
headers: { "x-webhook-secret": WEBHOOK_SECRET },
body: { eventId: "evt_1", orderId },
});
assert.equal(ok.res.status, 202);
const dup = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
headers: { "x-webhook-secret": WEBHOOK_SECRET },
body: { eventId: "evt_1", orderId },
});
assert.equal(dup.res.status, 200);
assert.equal(dup.json.duplicate, true);
});
test("異常系: 認証なし・入力不正・存在しないID", async (baseUrl) => {
const missingAuth = await requestJson(baseUrl, "/orders", {
method: "POST",
body: { items: [{ sku: "book", quantity: 1, priceCents: 1500 }] },
});
assert.equal(missingAuth.res.status, 401);
const token = await login(baseUrl);
const invalid = await requestJson(baseUrl, "/orders", { method: "POST", token, body: { items: [] } });
assert.equal(invalid.res.status, 400);
assert.equal(invalid.json.error.code, "validation_failed");
const notFound = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
headers: { "x-webhook-secret": WEBHOOK_SECRET },
body: { eventId: "evt_x", orderId: "ord_missing" },
});
assert.equal(notFound.res.status, 404);
});
await withServer(async (baseUrl) => {
let failed = 0;
for (const { name, fn } of tests) {
try {
await fn(baseUrl);
console.log(`ok - ${name}`);
} catch (error) {
failed += 1;
console.error(`not ok - ${name}`);
console.error(error);
}
}
if (failed > 0) process.exitCode = 1;
});
実行はこれだけです。
node api-smoke.test.mjs
成功すると4本が ok で並びます。これは本番APIを叩くテストではなく、型を体で覚えるための安全なモックです。実プロジェクトに移すときは、makeApp() の代わりにローカル起動したアプリ、ステージング環境、あるいはPlaywrightの request フィクスチャに差し替えます。Webhookの「署名なし・重複・存在しない注文ID」を入れている点に注目してください。成功パスだけ書くと、本番の事故にそっくりな条件を全部見逃します。
契約テストは「ドキュメントと実装のズレ確認」でしかない
契約テスト(contract test)は名前が厳めしいだけで、中身はシンプルです。「ドキュメント上の約束」と「実装が実際に返すもの」が同じかを見るだけ。
API変更が多いチームほど、実装だけ直してOpenAPIを更新し忘れる、という事故が起きます。レビューでもれなく見るために、僕は契約のかけらをClaude Codeに渡すようにしています。
openapi: 3.1.0
info:
title: Local Orders API
version: 1.0.0
paths:
/orders:
post:
responses:
"201":
description: Order created
content:
application/json:
schema:
type: object
required: [order]
properties:
order:
type: object
required: [id, status, totalCents, items]
この断片を渡すだけで、「作成は201」「レスポンスは order で包む」「order.id が必須」という前提を共有できます。変更の多いチームでは、実装・OpenAPI・テスト・READMEの4つが同時に変わっているかをレビュー観点にすると、ズレが減ります。土台の仕様はOpenAPI Specificationを、fetch の挙動で迷ったらMDN Fetch APIを見てください。
テストデータとモックの扱い方
APIテストが落ちる原因の大半は、ロジックではなくテストデータです。共有の [email protected] を全員で使い回すと、並列実行でセッションが消えたり、注文番号が衝突したりします。ローカルモックならMapで十分ですが、実アプリでは次のどれかを選びます。
| 方法 | 向いている場面 | 注意点 |
|---|---|---|
| テストごとにDBをリセット | 小さなサービス、CI | 本番DBには絶対に向けない |
| 使い捨てIDを発行 | 注文、Webhook、外部連携 | 後片付けの仕組みが要る |
| 読み取り専用fixture | カタログ、公開設定 | 更新系のテストには弱い |
| 外部APIをモック | 決済、メール、CRM | モックだけ信じると本番差分を逃す |
モックの線引きで僕が決めているのは一つだけ。自分のコードは本物で、他人のサーバーはモックです。決済プロバイダやメール送信を毎回本物で叩くと、相手の障害やレート制限でこちらのテストが落ちる。普段はモック、リリース前だけ限定的にステージングで実通信を残す、という層に分けると安定します。
Claude Codeに「薄いテスト」を書かせない依頼文
ここまで来ると、なぜ最初のClaude Codeが一行テストを返したかが分かります。「テスト書いて」としか言わなかったからです。先に縛りを渡せば、200だけのテストにはなりません。
APIテストを追加してください。
対象:
- ログインとセッション確認
- 注文作成API
- 支払いWebhook
- 過去バグの再発防止
必ず確認すること:
- 正常系のステータスコードとJSONの形(必須キー)
- 認証なし、入力不正、存在しないID、Webhook署名なし
- レスポンスにパスワードやシークレットを含めないこと
- テストデータが他のテストと衝突しないこと(ランダムID or テストごと初期化)
- ログにトークンやCookieを出さないこと
- CIで実行できるコマンド
作業後に、追加したテスト・失敗時に検知できる事故・実行コマンドを短くまとめてください。
「失敗時に検知できる事故を説明して」と添えるのがコツです。これを書かせると、Claude Code自身が「このテストは何を守っているか」を意識し始め、緑にするだけのテストが減ります。
CIで速いものから落とす
CIでは、速いスモークテストを先に実行します。重いE2Eを回す前に、APIの入口で落とせるからです。GitHub Actionsならこれだけ。
name: api-tests
on:
pull_request:
push:
branches: [main]
jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx vitest run # supertest版
- run: node api-smoke.test.mjs # fetch版
本番に近い環境を叩く場合は、ログにシークレットを出さないこともCI要件に入れてください。失敗時のデバッグ用に Authorization ヘッダーを丸ごと console.log するテストは、通っていてもCIログやチャットに漏れる地雷です。
よくある質問
Q. supertestとfetch、どちらを選べばいい? Expressやそれに近いフレームワークを使っていて、サーバーをポートにbindせず叩きたいならsupertestが手早いです。Fastify・Hono・別言語のサーバー、または既に起動済みのサーバーを叩くならfetchが素直。迷ったら、フレームワークに密着しないfetchから始めて困ったら乗り換えで十分です。
Q. APIテストとE2Eテストはどう住み分ける? APIテストはサーバーの入出力(ステータス・JSON・認証・異常系)を速く広く。E2Eは画面操作と通しの体験を、数を絞って。落ちたとき原因が早く分かるのはAPIテストなので、こちらを厚めにします。画面側はPlaywright E2Eに任せます。
Q. モックだけで十分? 速くて安定する反面、本番のヘッダー・タイムアウト・エラー形式との差分を隠します。普段はモック中心でいいですが、契約テストかステージングで最低限の実通信を残してください。モックだけを信じると、本番でだけ壊れます。
Q. テストがCIでだけ落ちるのは何が原因?
ほぼテストデータの共有か、テスト間の状態漏れです。beforeEach で毎回アプリを作り直す、IDを使い捨てにする、で大半は消えます。関数モックの巻き上げが原因のこともあるので、そこはVitestのモックとカバレッジを参照してください。
Q. 契約テストは最初から要る? 小さなチームなら後回しでOKです。ただしAPIの変更が増えてきたら、OpenAPIの必須フィールドと実レスポンスを突き合わせるだけでも入れる価値があります。実装・仕様・テスト・READMEがズレる事故が一気に減ります。
実際に試した結果
冒頭の一行テストから始めて、僕が一番効いたと感じたのは「ログイン・注文作成・Webhook・過去バグ」を1本のAPIテストに束ねたことでした。Node.jsのローカルサーバーで動かすと外部依存の揺れがなく、npx vitest run と node api-smoke.test.mjs だけで、ステータス・JSONの形・認証漏れ・署名なしWebhook・入力不正まで一気に確認できます。
逆に、200だけの確認に戻してみると、password を返す事故も重複Webhookの扱いも、きれいに見逃しました。緑は安心の色じゃない、と改めて思い知った瞬間です。賢いテストを一発で書かせようとするより、「何を守るか」を先に7点で固定して、薄いテストを門前で弾く。これが今の僕のやり方です。
チームで標準化まで進めたいなら、テストコードだけでなくプロンプト・OpenAPI更新・レビュー観点・CIの失敗時対応までそろえると効きます。実リポジトリに合わせた整備はClaude Code研修・相談で支援しているので、必要なら覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。