Writing Custom MSW Response Resolvers
Static fixture files fail to replicate production API behavior under complex state transitions. Writing custom MSW response resolvers provides deterministic control over request interception, payload transformation, and stateful routing. This methodology is foundational for teams implementing Advanced MSW Handler Patterns to simulate backend workflows without provisioning ephemeral infrastructure.
Resolver Execution Context and Lifecycle
The resolver function acts as the execution boundary within an MSW handler. It receives a Request object, context utilities, and optional lifecycle hooks, returning a standardized HttpResponse or undefined to delegate downstream. Proper initialization requires a disciplined approach to Tool-Specific Implementation & Setup to guarantee consistent runtime behavior across browser Service Workers and Node.js test environments.
Execution Rules:
- Always return
HttpResponseorundefined. Returningnullor throwing unhandled errors breaks the interception chain. - Access route parameters via
info.paramsand query strings vianew URL(info.request.url).searchParams. - Prevent memory leaks by scoping mutable state to the module level, not the resolver closure.
Step-by-Step Implementation Guide
Defining Dynamic Payload Generation
Static JSON is insufficient for mutation-heavy workflows. Parse request.json(), apply business rules, and return computed entities. Enforce strict contract alignment using TypeScript generics.
import { http, HttpResponse } from 'msw';
export const customUserResolver = http.get('/api/users/:id', ({ params, request }) => {
const userId = params.id;
const url = new URL(request.url);
const includeMetadata = url.searchParams.get('meta') === 'true';
const payload = {
id: userId,
name: 'Mock User',
status: 'active',
...(includeMetadata && { createdAt: new Date().toISOString() })
};
return HttpResponse.json(payload);
});
Managing In-Memory State
Stateful simulation requires a module-scoped registry. Initialize a mutable store outside the resolver scope to persist changes across sequential POST/PUT/PATCH requests. This accurately replicates session state without external databases.
import { http, HttpResponse, delay } from 'msw';
// Module-scoped state registry
const orderRegistry: Array<{ id: string; items: any[]; status: string; createdAt: string }> = [];
export const statefulOrderResolver = http.post('/api/orders', async ({ request }) => {
const body = await request.json();
await delay(1200); // Simulate network RTT
if (!body.items || body.items.length === 0) {
return HttpResponse.json({ error: 'Empty cart' }, { status: 400 });
}
const newOrder = {
id: crypto.randomUUID(),
...body,
status: 'pending',
createdAt: new Date().toISOString()
};
orderRegistry.push(newOrder);
return HttpResponse.json(newOrder, { status: 201 });
});
Simulating Latency and Network Failures
QA engineers and platform architects use resolvers to validate frontend resilience.
- Inject Deterministic Delays: Use
await delay(ms)to emulate realistic round-trip times and test loading states. - Force Error Boundaries: Return explicit status codes (
429,500,503) with structured error payloads. - Prevention Strategy: Never hardcode delays in production builds. Wrap latency injection in environment checks (
process.env.NODE_ENV === 'test').
Integration with Local Development Workflows
Register resolvers during application bootstrap. Activate conditionally via environment variables to prevent unintended interception in staging/production.
Fast Configuration:
import { setupWorker } from 'msw/browser';
import { customUserResolver, statefulOrderResolver } from './resolvers';
const worker = setupWorker(customUserResolver, statefulOrderResolver);
if (import.meta.env.VITE_ENABLE_MOCKS === 'true') {
worker.start({
onUnhandledRequest: 'warn', // Surface routing mismatches immediately
quiet: false
});
}
Pair with Vite/Next.js dev servers to ensure hot-module replacement (HMR) compatibility. Clear the module registry on HMR updates to prevent state duplication during development.
Validation and QA Best Practices
- Schema Enforcement: Run Zod or AJV validation inside resolvers before returning payloads. Reject malformed requests with
400 Bad Request. - Contract Drift Prevention: Cross-reference mock responses against OpenAPI specs. Automate contract tests that assert resolver outputs match frontend type definitions.
- State Reset Hooks: Expose a
resetState()utility for QA test suites to clear the in-memory registry between test cases.
Troubleshooting Common Resolver Issues
| Symptom | Root Cause | Fast Resolution |
|---|---|---|
| Unhandled Promise Rejection | Missing await on request.json() or async DB/file reads |
Ensure all async operations are awaited before returning HttpResponse |
| CORS Preflight Failures | Missing Access-Control-Allow-Origin headers |
Add headers: { 'Access-Control-Allow-Origin': '*' } to HttpResponse |
| Incorrect Content-Type | Browser defaults to text/plain |
Explicitly set Content-Type: application/json or use HttpResponse.json() |
| Resolver Not Triggering | Route mismatch or worker not started | Enable onUnhandledRequest: 'warn' and verify exact path/method alignment |
Prevention Checklist:
- Always inspect the Network tab to confirm
mswinterception precedes actual network fallbacks. - Use
worker.stop()in teardown scripts to prevent cross-test pollution. - Log
request.methodandrequest.urlduring development to trace routing logic.
Technical Specifications
- MSW Version: 2.x (Node & Browser)
- Runtime: ES2022+, Service Worker API or Node.js
fetchpolyfill - Testing Integration: Compatible with Vitest, Jest, Playwright, and Cypress via
setupServer - Performance: Keep in-memory stores under 10k records to avoid GC pauses during test runs.