Tips & Tricks (Aktualisiert: 3.6.2026)

API-Mocking mit MSW und Claude Code: Praxisleitfaden

Realistische API-Mocks mit MSW und Claude Code für Browser, Node-Tests, Auth, Fehlerfälle und CI.

API-Mocking mit MSW und Claude Code: Praxisleitfaden

MSW steht für Mock Service Worker. Im Browser fängt es HTTP-Anfragen über einen Service Worker ab; in Node.js-Tests greift es in die Module ein, die Anfragen auslösen. Dadurch können lokale Entwicklung, Vitest und CI dieselben API-Handler verwenden.

Mit Claude Code sollte das Ziel nicht nur eine feste JSON-Antwort sein. Ein guter Mock bildet Authentifizierung, Pagination, Validierung, Fehlerstatus, Netzwerkausfälle und Vertragsänderungen ab. Ein reiner 200 OK-Mock sieht schnell aus, prüft aber nicht die Zustände, in denen echte Oberflächen brechen.

Dieser Leitfaden nutzt die aktuelle MSW-2-API aus der offiziellen Dokumentation: http, HttpResponse, setupWorker und setupServer. Beginne mit MSW Quick start, danach mit Browser integration und Node.js integration. Für Fehlerfälle sind error responses und network errors relevant.

Passende Vertiefungen sind fortgeschrittene Vitest-Techniken, Playwright-E2E-Tests, API-Testautomatisierung und CI/CD-Einrichtung.

Einsatzfälle

FallWas simuliert wirdRisiko ohne Mock
UI vor fertigem BackendListe, Detail, Erstellung, leerer ZustandDie UI passt später nicht zum Vertrag
Auth und Rollen401, 403, rollenbezogene AntwortenAdmin-Aktionen erscheinen für falsche Nutzer
Fehler-UX500, 422, Netzwerkausfall, VerzögerungEndloses Laden oder fehlender Wiederholen-Button
Vertragsprüfung in CIJSON-Form, Pflichtfelder, StatuscodesAPI-Drift erreicht Produktion

Eine brauchbare Anfrage an Claude Code:

Erstelle einen Benutzer-API-Mock mit MSW 2.
Browser-Entwicklung und Vitest in Node sollen dieselbe handlers.ts nutzen.
Enthalten sein müssen Pflicht-Auth, Pagination, Rollenfilter, 422, 404, 500 und ein Netzwerkausfall-Test.
Nutze TypeScript und lasse keine undefinierten Projekttypen zurück.

Architektur

flowchart LR
  UI["Browser-UI"] --> Worker["setupWorker"]
  Test["Vitest / CI"] --> Server["setupServer"]
  Worker --> Handlers["MSW handlers.ts"]
  Server --> Handlers
  Handlers --> Contract["API-Vertrag: Status / JSON / Auth / Latenz"]

Installation

npm i -D msw vitest typescript
npx msw init public/ --save

Prüfe bei laufendem Dev-Server http://localhost:5173/mockServiceWorker.js. Wenn dort 404 erscheint, kann der Browser keine Anfragen abfangen.

Kopierbare Handler

Das Beispiel stellt eine kleine Benutzer-API bereit: Liste, Detail, Erstellung, Änderung und Löschung. Es enthält Auth, Pagination, Validierung, 404 und Verzögerung. Die absolute URL funktioniert auch mit nativem fetch in Node.

import { delay, http, HttpResponse } from "msw";

export const API_ORIGIN = "https://api.example.test";

type Role = "admin" | "editor" | "viewer";

export type User = {
  id: string;
  name: string;
  email: string;
  role: Role;
};

type CreateUserInput = {
  name: string;
  email: string;
  role?: Role;
};

type ErrorBody = {
  error: {
    code: string;
    message: string;
    requestId: string;
  };
};

type PageMeta = {
  total: number;
  page: number;
  perPage: number;
};

type UserListResponse = {
  data: User[];
  meta: PageMeta;
};

const seedUsers: User[] = [
  { id: "u_1", name: "Aki Tanaka", email: "aki@example.com", role: "admin" },
  { id: "u_2", name: "Bea Sato", email: "bea@example.com", role: "editor" },
  { id: "u_3", name: "Cal Mori", email: "cal@example.com", role: "viewer" },
];

let users: User[] = [...seedUsers];

const jsonError = (status: number, code: string, message: string) =>
  HttpResponse.json(
    { error: { code, message, requestId: "req_mock_001" } },
    { status }
  );

const requireAuth = (request: Request) => {
  const token = request.headers.get("authorization");
  return token === "Bearer demo-token"
    ? null
    : jsonError(401, "UNAUTHORIZED", "Missing or invalid bearer token");
};

const isRole = (value: string | null): value is Role =>
  value === "admin" || value === "editor" || value === "viewer";

export function resetMockData() {
  users = [...seedUsers];
}

export const handlers = [
  http.get(`${API_ORIGIN}/users`, async ({ request }) => {
    const authError = requireAuth(request);
    if (authError) return authError;

    await delay(120);
    const url = new URL(request.url);
    const page = Number(url.searchParams.get("page") ?? "1");
    const perPage = Number(url.searchParams.get("perPage") ?? "20");
    const role = url.searchParams.get("role");

    if (!Number.isInteger(page) || page < 1) {
      return jsonError(422, "INVALID_PAGE", "page must be a positive integer");
    }

    if (!Number.isInteger(perPage) || perPage < 1 || perPage > 50) {
      return jsonError(422, "INVALID_PER_PAGE", "perPage must be between 1 and 50");
    }

    if (role && !isRole(role)) {
      return jsonError(422, "INVALID_ROLE", "role must be admin, editor, or viewer");
    }

    const filtered = role ? users.filter((user) => user.role === role) : users;
    const start = (page - 1) * perPage;

    return HttpResponse.json({
      data: filtered.slice(start, start + perPage),
      meta: { total: filtered.length, page, perPage },
    });
  }),

  http.get(`${API_ORIGIN}/users/:id`, async ({ params, request }) => {
    const authError = requireAuth(request);
    if (authError) return authError;

    await delay(80);
    const user = users.find((item) => item.id === String(params.id));

    return user
      ? HttpResponse.json({ data: user })
      : jsonError(404, "USER_NOT_FOUND", "User was not found");
  }),

  http.post(`${API_ORIGIN}/users`, async ({ request }) => {
    const authError = requireAuth(request);
    if (authError) return authError;

    const body = (await request.json()) as Partial<CreateUserInput>;

    if (!body.name?.trim() || !body.email?.includes("@")) {
      return jsonError(422, "INVALID_INPUT", "name and a valid email are required");
    }

    if (body.role && !isRole(body.role)) {
      return jsonError(422, "INVALID_ROLE", "role must be admin, editor, or viewer");
    }

    const user: User = {
      id: `u_${Date.now()}`,
      name: body.name.trim(),
      email: body.email,
      role: body.role ?? "viewer",
    };

    users = [user, ...users];

    return HttpResponse.json({ data: user }, { status: 201 });
  }),

  http.patch(`${API_ORIGIN}/users/:id`, async ({ params, request }) => {
    const authError = requireAuth(request);
    if (authError) return authError;

    const index = users.findIndex((item) => item.id === String(params.id));
    if (index === -1) return jsonError(404, "USER_NOT_FOUND", "User was not found");

    const body = (await request.json()) as Partial<CreateUserInput>;

    if (body.email && !body.email.includes("@")) {
      return jsonError(422, "INVALID_EMAIL", "email must include @");
    }

    if (body.role && !isRole(body.role)) {
      return jsonError(422, "INVALID_ROLE", "role must be admin, editor, or viewer");
    }

    users[index] = { ...users[index], ...body };

    return HttpResponse.json({ data: users[index] });
  }),

  http.delete(`${API_ORIGIN}/users/:id`, async ({ params, request }) => {
    const authError = requireAuth(request);
    if (authError) return authError;

    users = users.filter((item) => item.id !== String(params.id));

    return new HttpResponse(null, { status: 204 });
  }),
];

Browser

Nutze setupWorker und warte auf worker.start(), bevor die Anwendung gerendert wird.

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";

async function enableMocking() {
  if (!import.meta.env.DEV || import.meta.env.VITE_API_MOCKING !== "enabled") {
    return;
  }

  const { worker } = await import("./mocks/browser");

  await worker.start({
    onUnhandledRequest: "bypass",
  });
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

Aktiviere den Mock explizit mit VITE_API_MOCKING=enabled npm run dev. In Produktion dürfen Login, Kauf, Formulare und Monetarisierungs-CTA nicht gegen Scheinantworten laufen.

Vitest und CI

import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { API_ORIGIN, handlers, resetMockData } from "../src/mocks/handlers";

const server = setupServer(...handlers);

function authed(input: string, init: RequestInit = {}) {
  const headers = new Headers(init.headers);
  headers.set("authorization", "Bearer demo-token");
  return fetch(input, { ...init, headers });
}

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));

afterEach(() => {
  server.resetHandlers();
  resetMockData();
});

afterAll(() => server.close());

describe("users API mock", () => {
  it("returns a paginated user list", async () => {
    const response = await authed(`${API_ORIGIN}/users?page=1&perPage=2`);
    const body = (await response.json()) as {
      data: Array<Record<string, unknown>>;
      meta: Record<string, unknown>;
    };

    expect(response.status).toBe(200);
    expect(body.data).toHaveLength(2);
    expect(body.meta).toMatchObject({ total: 3, page: 1, perPage: 2 });
  });

  it("rejects missing auth", async () => {
    const response = await fetch(`${API_ORIGIN}/users`);
    const body = (await response.json()) as { error: { code: string } };

    expect(response.status).toBe(401);
    expect(body.error.code).toBe("UNAUTHORIZED");
  });

  it("simulates a network failure for retry UI", async () => {
    server.use(
      http.get(`${API_ORIGIN}/users`, () => {
        return HttpResponse.error();
      })
    );

    await expect(authed(`${API_ORIGIN}/users`)).rejects.toThrow();
  });

  it("guards against response contract drift", async () => {
    const response = await authed(`${API_ORIGIN}/users`);
    const body = (await response.json()) as {
      data: Array<Record<string, unknown>>;
      meta: Record<string, unknown>;
    };

    expect(Object.keys(body.data[0]).sort()).toEqual(["email", "id", "name", "role"]);
    expect(body.data[0]).toEqual(
      expect.objectContaining({
        id: expect.any(String),
        email: expect.stringContaining("@"),
      })
    );
    expect(body.meta).toEqual(expect.objectContaining({ page: 1, perPage: 20 }));
  });
});
name: msw-contract

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test -- --run

Häufige Fehler

Erstens: mockServiceWorker.js ist nicht erreichbar. Bei 404 gibt es im Browser keine Interception.

Zweitens: Testzustand läuft aus. Nach jedem Test sollten server.resetHandlers() und die Datenrücksetzung laufen.

Drittens: onUnhandledRequest: "bypass" in CI. Tests sollten bei nicht gemockten Anfragen fehlschlagen.

Viertens: Auth wird nicht modelliert. Viele Fehler liegen zwischen gültiger Sitzung, abgelaufener Sitzung, fehlendem Recht und falscher Rolle.

Fünftens: Der Vertrag wird nicht geprüft. Prüfe data, meta.total und error.code, nicht nur den HTTP-Status.

Monetarisierungs-CTA

MSW ist besonders nützlich auf Umsatzpfaden: Artikel-CTA, Produktkauf, Kontaktformular, kostenlose Registrierung und Checkout-Vorschau. Simuliere 500, langsame Antworten, abgelaufene Auth und Validierungsfehler vor der Veröffentlichung. Für Claude-Code-Vorlagen und Review-Checklisten eignen sich Produkte oder Claude-Code-Training.

Ergebnis aus der Praxis

Als Masa diese Struktur für einen CTA- und Produktfluss nutzte, brachten HttpResponse.error() und onUnhandledRequest: "error" den größten Nutzen. Ein reiner Erfolgs-Mock übersah einen fehlenden Wiederholen-Button, einen fehlenden Auth-Header und ein entferntes meta.total. Gemeinsame Handler für lokale Entwicklung und CI machten die Fehler reproduzierbar.

#Claude Code #MSW #API-Mocking #Tests #Frontend
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.