Claude Code से API Testing Automation: Practical Guide
Claude Code से API testing सीखें: smoke tests, auth, JSON shape, contract tests, CI और runnable examples.
API testing का मतलब है कि browser screen खोलने से पहले server के promise को check करना। Test सीधे HTTP request भेजता है और देखता है कि login काम कर रहा है या नहीं, order बन रहा है या नहीं, error response समझने योग्य है या नहीं, authentication सच में लागू है या नहीं, और JSON shape client की expectation से मेल खाता है या नहीं।
Claude Code को अगर केवल “API tests लिख दो” कहा जाए, तो अक्सर output बहुत पतला होता है: एक happy path, एक 200 OK assertion, और बस। Real product को बचाने के लिए smoke tests, status codes, JSON shape, auth, negative tests, contract tests, test data और CI सभी चाहिए।
यह guide beginner-friendly है, लेकिन production सोच के साथ लिखी गई है। API design पर काम कर रहे हैं तो API design guide, breaking change से बचना है तो API versioning strategy, और failure investigation चाहिए तो error diagnosis workflow साथ पढ़ें।
Official docs को reference में रखना अच्छा रहता है। Playwright project में Playwright API testing देखें। नीचे का example Node.js fetch पर है, जिसका basis MDN Fetch API है। API contract के लिए OpenAPI Specification उपयोगी है।
API Test क्या साबित करे
API test का लक्ष्य बहुत सारे tests बनाना नहीं है। लक्ष्य है जल्दी जानना कि public contract टूट गया है या नहीं। Browser E2E tests जरूरी हैं, लेकिन fail होने पर root cause UI, network, session, backend, data या external provider कुछ भी हो सकता है। API test UI को skip करके server boundary को सीधे verify करता है।
| Check | आसान अर्थ | Example |
|---|---|---|
| Smoke test | service minimum level पर alive है | /health 204, login 200 |
| Status code | HTTP number से result | create 201, no auth 401, missing 404 |
| JSON shape | required keys और forbidden fields | sessionId है, password वापस नहीं आता |
| Authentication | caller की identity check | Bearer token, cookie, API key |
| Negative test | जानबूझकर गलत input भेजना | wrong password, empty order, unsigned webhook |
| Contract test | public promise और implementation match | OpenAPI required fields response में हैं |
| Test data | हर run predictable state से शुरू | local mock, reset DB, disposable order ID |
सबसे बड़ा trap है केवल 200 OK देखना। Response 200 हो सकता है, फिर भी field गायब हो सकती है, error format बदल सकता है, secret leak हो सकता है, या बिना auth request pass हो सकता है। Claude Code prompt में ये expectations साफ लिखना जरूरी है।
4 Practical Use Cases
इस article का sample चार common API flows को एक छोटे test suite में रखता है।
| Use case | क्यों important | क्या assert करें |
|---|---|---|
| Login/session smoke test | ज्यादातर features valid session पर depend करते हैं | 200, sessionId, user shape, no password |
| Order creation API | revenue, inventory, receipt और support से जुड़ा है | 201, Location header, total, detail fetch |
| Webhook endpoint | external services async call और retry करते हैं | no signature 401, valid event 202, duplicate safe |
| Regression test for bug | fixed bug वापस नहीं आना चाहिए | 400, 401, 404, stable error JSON |
Claude Code को task देते समय इन flows को नाम से लिखें। Webhook के लिए केवल success path काफी नहीं है। Missing signature, duplicate event ID और unknown order ID को जरूर cover करें।
flowchart LR
A["OpenAPI or API notes"] --> B["Claude Code prompt"]
B --> C["Local API test"]
C --> D["Negative tests"]
D --> E["CI gate"]
E --> F["Regression safety"]
Runnable Node fetch Example
यह file production service या real database को touch नहीं करती। यह छोटा local HTTP server start करती है और Node.js built-in fetch से login, order creation, webhook और negative cases test करती है। File को api-smoke.test.mjs नाम दें और Node.js 18 या newer से चलाएं।
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { createServer } from "node:http";
const TEST_USER = {
id: "user_1",
email: "demo@example.com",
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) {
resolve({});
return;
}
try {
resolve(JSON.parse(raw));
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function bearerToken(req) {
const value = req.headers.authorization;
if (typeof value === "string" && value.startsWith("Bearer ")) {
return value.slice("Bearer ".length);
}
return "";
}
function validateItems(items) {
if (!Array.isArray(items) || items.length === 0) {
return ["items must be a non-empty array"];
}
return items.flatMap((item, index) => {
const errors = [];
if (typeof item.sku !== "string" || item.sku.length === 0) {
errors.push(`items[${index}].sku must be a non-empty string`);
}
if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
errors.push(`items[${index}].quantity must be a positive integer`);
}
if (!Number.isInteger(item.priceCents) || item.priceCents < 0) {
errors.push(`items[${index}].priceCents must be a non-negative integer`);
}
return errors;
});
}
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: "Request body is not valid 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: "Email or password is wrong" },
});
}
const sessionId = `sess_${randomUUID()}`;
sessions.set(sessionId, { id: TEST_USER.id, email: TEST_USER.email });
return sendJson(res, 200, {
sessionId,
expiresIn: 3600,
user: { id: TEST_USER.id, email: TEST_USER.email },
});
}
if (method === "GET" && url.pathname === "/me") {
const user = currentUser();
if (!user) {
return sendJson(res, 401, {
error: { code: "unauthorized", message: "Bearer token is required" },
});
}
return sendJson(res, 200, { user });
}
if (method === "POST" && url.pathname === "/orders") {
const user = currentUser();
if (!user) {
return sendJson(res, 401, {
error: { code: "unauthorized", message: "Bearer token is required" },
});
}
const details = validateItems(body.items);
if (details.length > 0) {
return sendJson(res, 400, {
error: { code: "validation_failed", message: "Order payload is invalid", details },
});
}
const totalCents = body.items.reduce(
(sum, item) => sum + item.quantity * item.priceCents,
0,
);
const order = {
id: `ord_${orderSeq++}`,
userId: user.id,
status: "created",
totalCents,
items: body.items,
};
orders.set(order.id, order);
return sendJson(res, 201, { order }, { location: `/orders/${order.id}` });
}
const orderMatch = url.pathname.match(/^\/orders\/([^/]+)$/);
if (method === "GET" && orderMatch) {
const user = currentUser();
if (!user) {
return sendJson(res, 401, {
error: { code: "unauthorized", message: "Bearer token is required" },
});
}
const order = orders.get(orderMatch[1]);
if (!order || order.userId !== user.id) {
return sendJson(res, 404, {
error: { code: "order_not_found", message: "Order was not found" },
});
}
return sendJson(res, 200, { order });
}
if (method === "POST" && url.pathname === "/webhooks/payment") {
if (req.headers["x-webhook-secret"] !== WEBHOOK_SECRET) {
return sendJson(res, 401, {
error: { code: "bad_signature", message: "Webhook signature is invalid" },
});
}
if (typeof body.eventId !== "string" || typeof body.orderId !== "string") {
return sendJson(res, 400, {
error: { code: "validation_failed", message: "eventId and orderId are required" },
});
}
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: "Order was not found" },
});
}
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} is not supported` },
});
};
}
async function withServer(fn) {
const server = createServer(makeApp());
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
const baseUrl = `http://127.0.0.1:${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 };
}
function expectKeys(value, keys) {
for (const key of keys) {
assert.ok(Object.prototype.hasOwnProperty.call(value, key), `missing key: ${key}`);
}
}
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);
assert.equal(typeof json.sessionId, "string");
return json.sessionId;
}
async function createOrder(baseUrl, token) {
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_/);
expectKeys(json.order, ["id", "status", "totalCents", "items"]);
assert.equal(json.order.totalCents, 7000);
return json.order;
}
const tests = [];
function test(name, fn) {
tests.push({ name, fn });
}
test("login/session smoke test", 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);
expectKeys(json, ["sessionId", "expiresIn", "user"]);
assert.equal(json.user.email, TEST_USER.email);
assert.equal(json.user.password, undefined);
const me = await requestJson(baseUrl, "/me", { token: json.sessionId });
assert.equal(me.res.status, 200);
assert.equal(me.json.user.id, TEST_USER.id);
});
test("order creation API returns a stable JSON shape", async (baseUrl) => {
const token = await login(baseUrl);
const order = await createOrder(baseUrl, token);
const detail = await requestJson(baseUrl, `/orders/${order.id}`, { token });
assert.equal(detail.res.status, 200);
assert.equal(detail.json.order.id, order.id);
assert.equal(detail.json.order.status, "created");
});
test("payment webhook verifies signature and duplicate events", async (baseUrl) => {
const token = await login(baseUrl);
const order = await createOrder(baseUrl, token);
const noSignature = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
body: { eventId: "evt_1", orderId: order.id },
});
assert.equal(noSignature.res.status, 401);
assert.equal(noSignature.json.error.code, "bad_signature");
const accepted = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
headers: { "x-webhook-secret": WEBHOOK_SECRET },
body: { eventId: "evt_1", orderId: order.id },
});
assert.equal(accepted.res.status, 202);
assert.equal(accepted.json.duplicate, false);
const paidOrder = await requestJson(baseUrl, `/orders/${order.id}`, { token });
assert.equal(paidOrder.json.order.status, "paid");
const duplicate = await requestJson(baseUrl, "/webhooks/payment", {
method: "POST",
headers: { "x-webhook-secret": WEBHOOK_SECRET },
body: { eventId: "evt_1", orderId: order.id },
});
assert.equal(duplicate.res.status, 200);
assert.equal(duplicate.json.duplicate, true);
});
test("regression tests cover auth, validation, and not-found bugs", async (baseUrl) => {
const badLogin = await requestJson(baseUrl, "/login", {
method: "POST",
body: { email: TEST_USER.email, password: "wrong" },
});
assert.equal(badLogin.res.status, 401);
assert.equal(badLogin.json.error.code, "invalid_credentials");
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 invalidOrder = await requestJson(baseUrl, "/orders", {
method: "POST",
token,
body: { items: [{ sku: "book", quantity: 0, priceCents: 1500 }] },
});
assert.equal(invalidOrder.res.status, 400);
assert.equal(invalidOrder.json.error.code, "validation_failed");
assert.ok(Array.isArray(invalidOrder.json.error.details));
const missingOrder = await requestJson(baseUrl, "/orders/ord_missing", { token });
assert.equal(missingOrder.res.status, 404);
assert.equal(missingOrder.json.error.code, "order_not_found");
});
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;
}
});
Run command:
node api-smoke.test.mjs
Success पर 4 ok lines दिखेंगी। यह safe local mock है, production test नहीं। Real project में makeApp() को local app server, staging URL, या Playwright request fixture से बदल सकते हैं। Assertions वही रखें: status code, JSON shape, auth boundary, negative behavior और no leaked secrets।
Claude Code Prompt
Claude Code को target और failure rules दोनों दें। यह prompt एक single happy path से बचाता है।
Add API tests for these flows:
- login and session check
- order creation API
- payment webhook
- regression coverage for the last bug
Must verify:
- success status codes and JSON shape
- missing auth, invalid input, unknown ID, and missing webhook signature
- password, tokens, and secrets are never returned or logged
- test data does not collide across parallel test runs
- a command that CI can run
After the edit, summarize which incidents the tests would catch and which command verifies them.
अगर OpenAPI file है, तो लिखें कि OpenAPI को contract माना जाए। Contract test का अर्थ है कि public promise और actual response एक जैसे रहें।
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]
यह छोटा fragment बताता है कि creation 201 देगा, response order में wrapped होगा, और required fields मौजूद होंगे। Review करते समय implementation, OpenAPI, tests और README को साथ देखें।
Test Data और CI
API tests की stability अक्सर data पर निर्भर करती है। अगर हर test वही demo@example.com, वही order ID और वही webhook event ID इस्तेमाल करे, तो parallel CI flaky होगा। Disposable IDs, per-run DB reset, या local mock का उपयोग करें।
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: node api-smoke.test.mjs
Fast API smoke tests को CI की शुरुआत में चलाएं। इससे auth, JSON और webhook regression browser tests से पहले पकड़े जाते हैं। Staging call हो तो logs में token, cookie, webhook signature या API key print न करने का rule जोड़ें।
Common Pitfalls
पहला pitfall है केवल 200 OK check करना। Required fields, forbidden fields, error format और failure status codes भी check करें।
दूसरा है shared test data। एक global demo user tests को order-dependent और flaky बनाता है।
तीसरा है secrets in logs। Authorization, cookies और webhook signatures mask होने चाहिए।
चौथा है flaky external dependency। Payment, email और CRM को daily CI में mock करें, और release से पहले limited staging check रखें।
पांचवां है no negative tests। Missing auth, invalid payload, unknown ID, forbidden role और unsigned webhook API contract का हिस्सा हैं।
छठा है केवल mocks पर भरोसा। Mock fast है, लेकिन real headers, timeout, error envelope और status code difference छिपा सकता है।
Summary और CTA
Claude Code से अच्छे API tests पाने के लिए पहले वह promise लिखें जिसे protect करना है: login stable रहे, order creation सही रहे, webhook verify हो, और पुराने bugs regression tests से ढके रहें। फिर Claude Code से code और CI command बनवाएं।
Team rollout में असली benefit standardization है: prompts, OpenAPI updates, review criteria, CI gates और failure diagnosis। Real repository के लिए help चाहिए तो Claude Code training and consultation देखें। Solo builders free cheatsheet और इस article के prompt से शुरू कर सकते हैं।
Masa ने इस workflow को ऊपर दिए local Node server पर चलाकर verify किया। Practical result यह था कि login, order creation, webhook verification और regression coverage एक short command में आ गए: node api-smoke.test.mjs। यह 200 OK से ज्यादा पकड़ता है: leaked password, missing auth, unsigned webhook, invalid order payload और duplicate webhook event।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.