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 HttpResponse or undefined. Returning null or throwing unhandled errors breaks the interception chain.
  • Access route parameters via info.params and query strings via new 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:

  1. Always inspect the Network tab to confirm msw interception precedes actual network fallbacks.
  2. Use worker.stop() in teardown scripts to prevent cross-test pollution.
  3. Log request.method and request.url during development to trace routing logic.

Technical Specifications

  • MSW Version: 2.x (Node & Browser)
  • Runtime: ES2022+, Service Worker API or Node.js fetch polyfill
  • 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.