Use Cases (Aktualisiert: 2.6.2026)

API-Tests mit Claude Code: Praxisleitfaden zur Automatisierung

Praxisleitfaden für API-Tests mit Claude Code: Smoke Tests, Auth, JSON, Verträge, CI und lauffähige Beispiele.

API-Tests mit Claude Code: Praxisleitfaden zur Automatisierung

API-Tests prüfen, ob ein Server sein öffentliches Versprechen einhält, bevor ein Browser überhaupt eine Seite öffnet. Sie senden direkte HTTP-Anfragen und kontrollieren, ob Login funktioniert, ob Bestellungen erstellt werden, ob Fehler verständlich sind, ob Authentifizierung greift und ob die JSON-Struktur noch zu Frontend, Mobile-App oder Partnerintegration passt.

Wenn man Claude Code nur bittet, “API-Tests zu schreiben”, entsteht oft ein zu dünnes Ergebnis: ein Happy Path, eine 200-OK-Assertion und kaum Schutz gegen echte Produktionsfehler. Ein nützliches Testset braucht Smoke Tests, Statuscodes, JSON Shape, Authentifizierung, negative Tests, Contract Tests, Testdaten und CI.

Dieser Leitfaden zeigt einen anfängerfreundlichen, aber praktischen Workflow. Für verwandte Themen siehe den API-Design-Leitfaden, die API-Versionierungsstrategie und den Workflow zur Fehlerdiagnose.

Offizielle Referenzen sollten Teil der Arbeitsgrundlage sein. Wenn euer Projekt Playwright nutzt, ist Playwright API testing relevant. Das Beispiel unten nutzt fetch, beschrieben bei MDN Fetch API. Für prüfbare API-Verträge ist die OpenAPI Specification die Basis.

Was API-Tests beweisen sollen

Das Ziel ist nicht eine hohe Testzahl, sondern frühe Klarheit darüber, ob ein Vertrag gebrochen wurde. E2E-Tests im Browser bleiben wertvoll, weil sie die komplette Benutzerreise prüfen. Bei Fehlern ist die Ursache aber oft schwer zu trennen: UI, Netzwerk, Session, Backend, Testdaten oder externer Anbieter. API-Tests überspringen die UI und prüfen direkt die Servergrenze.

PrüfungEinfache BedeutungBeispiel
Smoke TestMinimaler Lebenszeichen-Test/health liefert 204, Login liefert 200
StatuscodeErgebnis als HTTP-ZahlErstellung 201, ohne Auth 401, nicht gefunden 404
JSON ShapePflichtfelder und verbotene FeldersessionId existiert, password wird nicht zurückgegeben
AuthentifizierungIdentität des Aufrufers wird geprüftBearer Token, Cookie oder API Key
Negativer TestFehlerhafte Eingaben bewusst sendenFalsches Passwort, leere Bestellung, unsignierter Webhook
Contract TestImplementierung passt zum öffentlichen VertragOpenAPI-Pflichtfelder sind weiterhin vorhanden
TestdatenJeder Lauf startet aus einem bekannten ZustandLokaler Mock, resetbare DB, Wegwerf-ID

Der häufigste Fehler ist, nur 200 OK zu prüfen. Eine Antwort kann erfolgreich sein und trotzdem kaputt: ein Feld fehlt, ein Error Envelope wurde geändert, ein Secret wird geleakt oder ein unauthentifizierter Request wird akzeptiert. Diese Erwartungen gehören explizit in den Prompt an Claude Code.

Vier praktische Use Cases

Das Beispiel in diesem Artikel fasst vier API-Flows zusammen, die in kleinen Test-Suites besonders viel Schutz bieten.

Use CaseWarum wichtigWas prüfen
Login und Session als Smoke TestFast alle Funktionen hängen an einer gültigen Session200, sessionId, User-Shape, kein Passwort
Bestell-APIBetrifft Umsatz, Bestand, Belege und Support201, Location-Header, Summe, Detailabruf
Webhook-EndpunktExterne Services rufen ihn asynchron auf und wiederholenOhne Signatur 401, gültiges Event 202, Duplikat sicher
Regressionstest für BugEin behobener Fehler darf nicht unbemerkt zurückkehren400, 401, 404 und stabiles Fehler-JSON

Benennen Sie diese Flows direkt in der Aufgabe an Claude Code. Gerade bei Webhooks ist ein Happy Path zu wenig. Fehlende Signatur, doppelte Event-ID und unbekannte Ressource müssen Teil des Tests sein.

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"]

Ausführbares Beispiel mit Node fetch

Diese Datei spricht keine Produktion und keine echte Datenbank an. Sie startet einen kleinen lokalen HTTP-Server und testet ihn mit dem in Node.js eingebauten fetch. Speichern Sie die Datei als api-smoke.test.mjs und verwenden Sie Node.js 18 oder neuer.

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;
  }
});

Ausführen:

node api-smoke.test.mjs

Bei Erfolg erscheinen vier ok-Zeilen. Das ist ein sicherer lokaler Mock, kein Test gegen Produktion. In einem echten Repository ersetzen Sie makeApp() durch den lokalen App-Server, eine Staging-URL oder Playwrights request-Fixture. Die Assertions bleiben gleich: Statuscode, JSON Shape, Auth-Grenze, negative Fälle und keine geleakten Secrets.

Guter Prompt für Claude Code

Claude Code braucht Ziel und Fehlregeln. Ein konkreter Prompt verhindert, dass nur der Happy Path entsteht.

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.

Wenn eine OpenAPI-Datei existiert, schreiben Sie dazu: “Treat OpenAPI as the contract.” Ein Contract Test heißt nur, dass öffentliche Spezifikation und tatsächliche Antwort übereinstimmen.

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]

Dieses kleine Fragment legt bereits fest: Erstellung liefert 201, der Body ist in order eingepackt, Pflichtfelder müssen vorhanden sein. Prüfen Sie Implementierung, OpenAPI, Tests und README gemeinsam.

Testdaten und CI

Die Stabilität von API-Tests hängt stark an Daten. Wenn alle Tests demo@example.com, dieselbe Bestell-ID und dieselbe Webhook-Event-ID nutzen, wird paralleles CI instabil. Verwenden Sie Wegwerf-IDs, setzen Sie Datenbanken pro Lauf zurück oder modellieren Sie kleine Flows mit einem lokalen 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

Schnelle API-Smoke-Tests gehören früh in die CI-Pipeline. So fallen Auth-, JSON- und Webhook-Probleme vor langsamen Browsertests auf. Wenn Staging aufgerufen wird, muss außerdem gelten: Tokens, Cookies, Webhook-Signaturen und API Keys niemals in Logs schreiben.

Häufige Fehler

Erstens: nur 200 OK prüfen. Pflichtfelder, verbotene Felder, Fehlerformat und Statuscodes für Fehler gehören dazu.

Zweitens: gemeinsame Testdaten. Ein globaler Demo-User macht Tests abhängig von Reihenfolge und Parallelität.

Drittens: Secrets in Logs. Authorization, Cookies und Webhook-Signaturen müssen maskiert werden.

Viertens: instabile externe Abhängigkeiten. Zahlung, E-Mail und CRM sollten im täglichen CI meist gemockt und vor Release gezielt in Staging geprüft werden.

Fünftens: keine negativen Tests. Fehlende Auth, ungültige Payloads, unbekannte IDs, verbotene Rollen und unsignierte Webhooks sind Teil des Vertrags.

Sechstens: nur Mocks glauben. Mocks sind schnell, können aber echte Header, Timeouts, Error Envelopes und Statuscodes verstecken.

Fazit und CTA

Gute API-Tests mit Claude Code beginnen mit der geschützten Zusage: Login bleibt gültig, Bestellungen werden stabil erstellt, Webhooks werden geprüft und alte Bugs sind als Regression abgedeckt. Danach lässt man Claude Code Code und CI-Befehl ergänzen.

Für Teams ist der größte Gewinn Standardisierung: Prompts, OpenAPI-Updates, Review-Kriterien, CI-Gates und Fehlerdiagnose. ClaudeCodeLab unterstützt über Claude Code training and consultation bei der Anpassung an echte Repositories. Einzelne Entwickler können mit dem kostenlosen Cheatsheet und dem Prompt aus diesem Artikel starten.

Masa hat den Workflow mit dem lokalen Node-Server oben getestet. Der praktische Effekt war, Login, Bestellerstellung, Webhook-Prüfung und Regressionen in eine kurze Anweisung zu bringen: node api-smoke.test.mjs. Das erkennt mehr als 200 OK: geleakte password-Felder, fehlende Auth, unsignierte Webhooks, ungültige Payloads und doppelte Webhook-Events.

#Claude Code #API testing #automation #testing #quality assurance
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.