Skip to main content

Mock HTTP Requests in React Tests: Step-by-Step

HTTP request mocking in MSW works by defining handlers that match specific method and URL patterns, then returning controlled responses that your React components can process predictably. Each handler receives the incoming request, can inspect its headers, body, or URL parameters, and returns an HttpResponse with a status code and JSON or text body.

I built a real-time collaboration feature for a SaaS product where tests needed to verify different API response times and failure modes. MSW's request interception made it possible to simulate slow networks and transient errors reproducibly, catching race-condition bugs that would have leaked into production.

Handler Structure: Methods, URLs, and Responses

MSW's http object provides methods for each HTTP verb. The handler callback receives a context object containing the request and URL parameters.

import { http, HttpResponse } from "msw";

export const handlers = [
// Simple GET
http.get("/api/articles/:id", ({ params }) => {
return HttpResponse.json(
{ id: params.id, title: "Sample Article", body: "Content here" },
{ status: 200 }
);
}),

// POST with request body inspection
http.post("/api/articles", async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: Math.random(), ...body, created: true },
{ status: 201 }
);
}),

// PUT to update
http.put("/api/articles/:id", async ({ request, params }) => {
const updates = await request.json();
return HttpResponse.json(
{ id: params.id, ...updates, updated: true },
{ status: 200 }
);
}),

// DELETE
http.delete("/api/articles/:id", ({ params }) => {
return HttpResponse.json({ deleted: true, id: params.id }, { status: 204 });
}),
];

The { params } object contains URL path parameters like :id. The request object (async) contains json(), text(), or formData() methods to read the body. Always await request.json() because reading the request body is asynchronous.

Matching Dynamic URL Patterns

MSW supports wildcards and regex patterns for flexible URL matching. This is crucial when your API calls include query strings or complex hierarchical paths.

// Match any suffix after /api/search
http.get("/api/search/*", ({ request }) => {
const url = new URL(request.url);
const query = url.searchParams.get("q");
return HttpResponse.json(
{ results: [`Result for ${query}`], count: 1 },
{ status: 200 }
);
});

// Match nested resource endpoints
http.get("/api/projects/:projectId/tasks/:taskId", ({ params }) => {
return HttpResponse.json({
projectId: params.projectId,
taskId: params.taskId,
title: "Task Alpha",
});
});

// Match a specific host (useful for multi-domain setups)
http.post(
"https://api.external-service.com/webhooks",
() => {
return HttpResponse.json({ acknowledged: true }, { status: 202 });
}
);

Patterns are evaluated in order, so place more specific patterns before generic ones. A pattern like /api/* matches anything under /api/, making it a good fallback for testing unhandled routes.

Inspecting and Validating Requests

Often you want to verify that your component sent the correct headers, body, or query parameters. MSW allows you to inspect the request and assert its contents in the handler itself or store it for later verification.

let capturedRequest = null;

export const handlers = [
http.post("/api/feedback", async ({ request }) => {
capturedRequest = { url: request.url, headers: request.headers };

// Verify the request includes an Authorization header
const auth = request.headers.get("Authorization");
if (!auth || !auth.startsWith("Bearer ")) {
return HttpResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Read and validate body
const body = await request.json();
if (!body.message || body.message.length < 5) {
return HttpResponse.json(
{ error: "Message too short" },
{ status: 400 }
);
}

return HttpResponse.json(
{ id: "feedback-1", accepted: true },
{ status: 201 }
);
}),
];

// In your test
test("sends Authorization header with feedback", async () => {
// Your component sends a fetch request with Authorization
// (implementation not shown)
render(<FeedbackForm />);
await userEvent.type(screen.getByRole("textbox"), "This is great feedback");
await userEvent.click(screen.getByRole("button", { name: /submit/i }));

await waitFor(() => {
expect(capturedRequest.headers.get("Authorization")).toMatch(/^Bearer /);
});
});

This pattern captures the request for inspection after the handler runs. It is particularly useful for testing that your component correctly sets authentication headers, custom headers, or request bodies.

Returning Different Responses Based on Input

Real APIs return different responses based on input. Your handlers should mirror this behavior to catch bugs where components do not handle variations correctly.

http.get("/api/users/:id", ({ params }) => {
const userId = parseInt(params.id, 10);

// Simulate user not found
if (userId === 999) {
return HttpResponse.json(
{ error: "User not found" },
{ status: 404 }
);
}

// Simulate a specific edge case
if (userId === 1) {
return HttpResponse.json(
{
id: userId,
name: "Admin User",
roles: ["admin", "user"],
permissions: "*",
},
{ status: 200 }
);
}

// Default response
return HttpResponse.json(
{
id: userId,
name: `User ${userId}`,
roles: ["user"],
permissions: ["read"],
},
{ status: 200 }
);
});

By returning different responses based on the request parameters, you can test edge cases (missing resources, permissions checks, special roles) without separate API servers or database state manipulation.

Setting Custom Response Headers

HTTP headers (like Content-Type, X-RateLimit-Remaining, or Set-Cookie) can affect component behavior. MSW allows you to include them in the response.

http.get("/api/rate-limited", () => {
return new HttpResponse(
JSON.stringify({ data: "value" }),
{
status: 200,
headers: {
"Content-Type": "application/json",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": "1719939600",
},
}
);
});

When you need both a body and custom headers, use the HttpResponse constructor directly instead of HttpResponse.json(). This is essential for testing rate-limit handling or CORS headers.

Key Takeaways

  • MSW handlers match HTTP methods (GET, POST, PUT, DELETE) and URL patterns, allowing granular control over mocked responses.
  • URL parameters are extracted via params; query strings and bodies are accessed via the request object.
  • Handlers can inspect headers and body content, validate them, and return different responses based on the input—mirroring real API behavior.
  • Custom response headers can be set to test header-dependent logic (rate limiting, authentication challenges).
  • Capturing requests in handlers enables post-test assertions that your component sent the correct data.

Frequently Asked Questions

How do I mock a slow network response?

Use http.get(..., async () => { await new Promise(resolve => setTimeout(resolve, 2000)); return HttpResponse.json(...); }) to add a delay. This tests timeout and loading-state behavior.

Can I mock different responses for the same endpoint in one test?

Yes, use server.use() to override a handler for a specific test. After the test, afterEach() resets all handlers to their defaults, as shown in the setup guide.

What happens if my component calls an endpoint with no matching handler?

MSW logs a warning and returns a 500 error. To catch unhandled routes, add a wildcard fallback handler at the end of your handlers array that returns a 404.

How do I mock file uploads or multipart form data?

Use request.formData() to parse the multipart body: const formData = await request.formData(); const file = formData.get("file");

Further Reading