Tips & Tricks (Mis à jour: 03/06/2026)

Mock d'API avec MSW et Claude Code : guide pratique

Créez des mocks d'API réalistes avec MSW et Claude Code pour navigateur, tests Node, erreurs, auth et CI.

Mock d'API avec MSW et Claude Code : guide pratique

MSW signifie Mock Service Worker. Dans le navigateur, il intercepte les requêtes HTTP avec un Service Worker. Dans les tests Node.js, il intercepte les modules qui émettent les requêtes. Le bénéfice est simple : le développement local, Vitest et la CI peuvent partager les mêmes gestionnaires d’API.

Avec Claude Code, il ne faut pas demander seulement une réponse JSON fixe. Un mock utile couvre l’authentification, la pagination, la validation, les statuts d’erreur, les pannes réseau et les dérives de contrat. Sinon, l’interface semble prête alors qu’elle n’a testé que le chemin heureux.

Ce guide utilise l’API MSW 2 confirmée dans la documentation officielle : http, HttpResponse, setupWorker et setupServer. Consultez MSW Quick start, Browser integration et Node.js integration. Pour les échecs, référez-vous à error responses et network errors.

Pour compléter, lisez aussi techniques avancées Vitest, tests E2E Playwright, automatisation des tests API et configuration CI/CD.

Cas d’usage

CasCe que l’on simuleRisque si absent
Interface avant le backendListe, détail, création, état videContrat incompatible lors du branchement réel
Authentification et rôles401, 403, données par rôleActions admin visibles au mauvais utilisateur
Expérience en panne500, 422, panne réseau, lenteurChargement infini ou bouton de reprise absent
Garde de contrat en CIForme JSON, champs requis, statutsChangement d’API livré sans alerte

Demandez à Claude Code quelque chose de précis :

Crée un mock d'API utilisateurs avec MSW 2.
Le même handlers.ts doit être partagé entre le navigateur et Vitest dans Node.
Ajoute authentification obligatoire, pagination, filtre de rôle, 422, 404, 500 et un test de panne réseau.
Utilise TypeScript sans laisser de types applicatifs manquants.

Architecture

flowchart LR
  UI["Interface navigateur"] --> Worker["setupWorker"]
  Test["Vitest / CI"] --> Server["setupServer"]
  Worker --> Handlers["MSW handlers.ts"]
  Server --> Handlers
  Handlers --> Contract["Contrat API : statut / JSON / auth / latence"]

Installation

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

Vérifiez http://localhost:5173/mockServiceWorker.js pendant que le serveur local tourne. Si la réponse est 404, le navigateur ne pourra pas intercepter les requêtes.

Gestionnaires copiables

Cet exemple fournit une petite API utilisateurs : liste, détail, création, mise à jour et suppression. Il inclut auth, pagination, validation, 404 et latence. L’URL absolue fonctionne aussi avec fetch natif dans 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 });
  }),
];

Utilisez setupWorker et attendez worker.start() avant de rendre l’application.

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

Activez-le explicitement avec VITE_API_MOCKING=enabled npm run dev. En production, les parcours de connexion, achat, formulaire et CTA doivent appeler les vrais services.

Vitest et 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

Pièges courants

Le premier piège est l’absence de mockServiceWorker.js. Si le fichier est en 404, aucune interception navigateur n’a lieu.

Le deuxième est la fuite d’état entre tests. Exécutez server.resetHandlers() et réinitialisez les données en mémoire après chaque test.

Le troisième est onUnhandledRequest: "bypass" en CI. En test, une requête non simulée doit échouer.

Le quatrième est l’oubli de l’authentification. Les bugs réels apparaissent souvent entre session valide, session expirée, droit insuffisant et mauvais rôle.

Le cinquième est l’absence de garde de contrat. Vérifiez data, meta.total et error.code, pas seulement le statut HTTP.

CTA de monétisation

MSW est très utile sur les chemins de revenus : CTA d’article, achat de produit, formulaire de contact, essai gratuit et étape avant paiement. Simulez 500, lenteur, session expirée et erreurs de validation avant publication. Pour transformer ce flux en modèles Claude Code et listes de revue, consultez les produits ou la formation Claude Code.

Résultat vérifié

Quand Masa a testé cette structure sur un parcours de CTA et de produit, HttpResponse.error() et onUnhandledRequest: "error" ont apporté le plus de valeur. Un mock de succès seul ne voyait pas le bouton de reprise manquant, l’en-tête d’authentification absent ni la disparition de meta.total. Le partage des mêmes gestionnaires entre local et CI a rendu les échecs reproductibles.

#Claude Code #MSW #mock d'API #tests #frontend
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.