Tips & Tricks (Atualizado: 03/06/2026)

Mock de API com MSW e Claude Code: guia prático

Crie mocks de API realistas com MSW e Claude Code para navegador, testes Node, autenticação, erros e CI.

Mock de API com MSW e Claude Code: guia prático

MSW significa Mock Service Worker. No navegador, ele intercepta requisições HTTP com um Service Worker; em testes Node.js, intercepta os módulos que fazem requisições no processo atual. Assim, desenvolvimento local, Vitest e CI podem usar os mesmos handlers de API.

Com Claude Code, o objetivo não é apenas gerar um JSON fixo. Um mock útil cobre autenticação, paginação, validação, status de erro, falha de rede e desvio de contrato. Se o mock só retorna 200 OK, a interface parece pronta, mas os casos que quebram em produção continuam invisíveis.

Este guia usa a API atual do MSW 2: http, HttpResponse, setupWorker e setupServer. Consulte MSW Quick start, Browser integration e Node.js integration. Para falhas, veja error responses e network errors.

Para aprofundar, veja também técnicas avançadas de Vitest, testes E2E com Playwright, automação de testes de API e configuração de CI/CD.

Casos de uso

CasoO que simularRisco se faltar
UI antes do backendLista, detalhe, criação, estado vazioA tela não combina com a API real
Autenticação e papéis401, 403, resposta por papelAção de admin aparece para usuário comum
Experiência em erro500, 422, falha de rede, atrasoCarregamento infinito ou botão de tentar de novo ausente
Contrato em CIFormato JSON, campos obrigatórios, statusMudança de API chega à produção sem aviso

Peça ao Claude Code com precisão:

Crie um mock de API de usuários com MSW 2.
O mesmo handlers.ts deve ser usado no navegador e no Vitest em Node.
Inclua autenticação obrigatória, paginação, filtro por papel, 422, 404, 500 e teste de falha de rede.
Use TypeScript sem deixar tipos de aplicação indefinidos.

Arquitetura

flowchart LR
  UI["Interface no navegador"] --> Worker["setupWorker"]
  Test["Vitest / CI"] --> Server["setupServer"]
  Worker --> Handlers["MSW handlers.ts"]
  Server --> Handlers
  Handlers --> Contract["Contrato API: status / JSON / auth / latência"]

Instalação

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

Abra http://localhost:5173/mockServiceWorker.js com o servidor local rodando. Se retornar 404, o navegador não conseguirá interceptar as requisições.

Handlers copiáveis

O exemplo abaixo implementa lista, detalhe, criação, atualização e exclusão de usuários. Ele inclui autenticação, paginação, validação, 404 e latência. A URL absoluta facilita o uso com fetch nativo em 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 });
  }),
];

Use setupWorker e espere worker.start() antes de renderizar a aplicação.

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

Ative explicitamente com VITE_API_MOCKING=enabled npm run dev. Em produção, login, compra, formulário e CTA de monetização precisam chamar serviços reais.

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

Armadilhas comuns

A primeira é não publicar mockServiceWorker.js. Se ele retorna 404, o navegador não intercepta nada.

A segunda é vazar estado entre testes. Chame server.resetHandlers() e reinicie os dados em memória após cada caso.

A terceira é usar onUnhandledRequest: "bypass" em CI. Em testes, requisições não mockadas devem falhar.

A quarta é ignorar autenticação. Muitos defeitos aparecem entre sessão válida, sessão expirada, falta de permissão e papel errado.

A quinta é não validar o contrato. Verifique data, meta.total e error.code, não apenas o status HTTP.

CTA de monetização

MSW é especialmente útil em caminhos de receita: CTA de artigo, compra de produto, formulário de contato, teste gratuito e prévia de checkout. Simule 500, lentidão, autenticação expirada e erro de validação antes de publicar. Para transformar isso em prompts e checklists de Claude Code, veja produtos ou treinamento Claude Code.

Resultado prático

Quando Masa testou essa estrutura em um fluxo de CTA e produto, HttpResponse.error() e onUnhandledRequest: "error" foram os pontos mais valiosos. Um mock só de sucesso não detectava botão de tentar novamente ausente, cabeçalho de autenticação perdido nem remoção de meta.total. Compartilhar handlers entre desenvolvimento local e CI tornou as falhas reproduzíveis.

#Claude Code #MSW #mock de API #testes #frontend
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.