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.
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
| Caso | O que simular | Risco se faltar |
|---|---|---|
| UI antes do backend | Lista, detalhe, criação, estado vazio | A tela não combina com a API real |
| Autenticação e papéis | 401, 403, resposta por papel | Ação de admin aparece para usuário comum |
| Experiência em erro | 500, 422, falha de rede, atraso | Carregamento infinito ou botão de tentar de novo ausente |
| Contrato em CI | Formato JSON, campos obrigatórios, status | Mudanç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 });
}),
];
Navegador
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.
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.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Escada de segurança de permissões no Claude Code
Amplie de read-only para edições limitadas, comandos de prova e deploy checks sem perder controle.
Claude Code Small PR Proof Pack: pequenas mudanças fáceis de revisar
Um pacote de prova para PRs do Claude Code: diff, checks, URL pública, CTA e rollback.
Gate de revisão antes do commit com Claude Code
Revisão antes do commit com Claude Code: diff, build, URL pública, Gumroad, consultoria, testes e arquivos fora do escopo.