Tips & Tricks (Actualizado: 3/6/2026)

Mock de API con MSW y Claude Code: guía práctica

Crea mocks de API realistas con MSW y Claude Code para navegador, pruebas Node, autenticación, errores y CI.

Mock de API con MSW y Claude Code: guía práctica

MSW significa Mock Service Worker. En el navegador intercepta peticiones HTTP con un Service Worker; en pruebas Node.js intercepta los módulos que emiten peticiones dentro del proceso. La ventaja práctica es que desarrollo local, Vitest y CI pueden usar los mismos controladores de API.

Con Claude Code, el objetivo no debe ser generar una respuesta JSON fija. Un mock útil cubre autenticación, paginación, validación, estados de error, fallos de red y cambios de contrato. Si solo devuelves 200 OK, la interfaz parece terminada, pero no has probado los casos que rompen una pantalla real.

Esta guía usa la API actual de MSW 2: http, HttpResponse, setupWorker y setupServer. Revisa MSW Quick start, Browser integration y Node.js integration. Para errores, usa error responses y network errors.

También conviene leer técnicas avanzadas de Vitest, pruebas E2E con Playwright, automatización de pruebas API y configuración de CI/CD.

Casos de uso

CasoQué simularRiesgo si falta
UI antes del backendLista, detalle, creación, estado vacíoLa UI no coincide con el contrato real
Autenticación y permisos401, 403, respuestas por rolAcciones de administrador visibles para usuarios normales
Experiencia ante fallos500, 422, caída de red, latenciaCarga infinita o botón de reintento ausente
Guardia de contrato en CIForma JSON, campos obligatorios, estadosUn cambio de API llega a producción sin aviso

Una petición útil para Claude Code:

Crea un mock de API de usuarios con MSW 2.
Comparte el mismo handlers.ts entre desarrollo en navegador y Vitest en Node.
Incluye autenticación obligatoria, paginación, filtro por rol, 422, 404, 500 y una prueba de error de red.
Usa TypeScript y no dejes tipos de aplicación sin definir.

Arquitectura

flowchart LR
  UI["Interfaz en navegador"] --> Worker["setupWorker"]
  Test["Vitest / CI"] --> Server["setupServer"]
  Worker --> Handlers["MSW handlers.ts"]
  Server --> Handlers
  Handlers --> Contract["Contrato API: estado / JSON / auth / latencia"]

Instalación

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

Abre http://localhost:5173/mockServiceWorker.js con el servidor local activo. Si responde 404, el navegador no podrá interceptar peticiones.

Controladores listos para copiar

Este ejemplo implementa usuarios con lista, detalle, creación, actualización y borrado. Incluye autenticación, paginación, validación, 404 y latencia. La URL es absoluta para que funcione también con fetch nativo en 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 });
  }),
];

Usa setupWorker y espera a worker.start() antes de renderizar la aplicación.

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

Actívalo de forma explícita con VITE_API_MOCKING=enabled npm run dev. No lo dejes activo en producción: login, compra, formularios y CTA de monetización deben usar servicios reales.

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

Errores frecuentes

El primero es no publicar mockServiceWorker.js. Si devuelve 404, el navegador no intercepta nada.

El segundo es compartir estado entre pruebas. Usa server.resetHandlers() y reinicia los datos en memoria después de cada caso.

El tercero es usar onUnhandledRequest: "bypass" en CI. En pruebas debe ser error para detectar peticiones no simuladas.

El cuarto es no modelar autenticación. Muchas incidencias viven entre sesión válida, sesión caducada, permiso insuficiente y rol incorrecto.

El quinto es no comprobar el contrato. Verifica data, meta.total y error.code, no solo el estado HTTP.

CTA de monetización

MSW es muy útil en rutas de ingresos: CTA de artículos, compras, formularios de contacto, pruebas gratuitas y pasos previos al pago. Simula 500, lentitud, sesión caducada y errores de validación antes de publicar. Para convertir este flujo en plantillas y listas de revisión de Claude Code, consulta productos o formación de Claude Code.

Resultado probado

Cuando Masa probó esta estructura en un flujo de CTA y producto, lo más valioso fue combinar HttpResponse.error() con onUnhandledRequest: "error". El mock de solo éxito no detectaba un botón de reintento ausente, una cabecera de autenticación perdida ni la desaparición de meta.total. Compartir controladores entre desarrollo local y CI hizo que los fallos fueran reproducibles.

#Claude Code #MSW #mock de API #pruebas #frontend
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.