Request Mocking Protocol (RMP) is a specification for declarative mocking of HTTP requests. It uses JSON schemas to define request matchers and response builders. These schemas can be serialized and sent over the network, enabling both client-side and server-side mocking (e.g., in React Server Components).
Click to expand
- Server-side mocking – Transmit mocks via a custom HTTP header to apply them on the server.
- Per-test isolation – Define mocks inside each test, enabling full parallel test execution.
- Test runner support – Works with Playwright, Cypress, and custom runners.
- Framework-agnostic – Built-in support for Next.js and Astro, or integrate with any framework.
- Request matching – Match requests by URL, wildcard, query, headers, or body.
- Response patching – Fetch real API responses and override only what’s needed.
- Dynamic parameters – Use
{{ }}
placeholders to inject route/query values into responses. - Mocks API – Set up mocks easily using a
MockClient
class. - Debug-friendly – Add
debug: true
for detailed breakdown of the mocking process.
- The test runner defines a request mock as a JSON object.
- The mock is sent with the page navigation via a custom HTTP header.
- The server reads the header and applies the mock to outgoing API requests.
- The page loads with data from the mocked response.
Check out the Concepts and Limitations for more details.
npm i -D request-mocking-protocol
RMP is designed to work seamlessly with popular test runners like Playwright and Cypress, and can also be integrated with custom runners.
Each test defines its own mocks using a MockClient
class. Mocks are not shared across tests, enabling per-test mock isolation and full parallelization.
-
Set up a custom fixture
mockServerRequest
:import { test as base } from '@playwright/test'; import { MockClient } from 'request-mocking-protocol'; export const test = base.extend<{ mockServerRequest: MockClient }>({ mockServerRequest: async ({ context }, use) => { const mockClient = new MockClient(); mockClient.onChange = async (headers) => context.setExtraHTTPHeaders(headers); await use(mockClient); }, });
-
Use
mockServerRequest
in test to define server-side mocks:test('my test', async ({ page, mockServerRequest }) => { // set up server-side mock await mockServerRequest.GET('https://jsonplaceholder.typicode.com/users', { body: [{ id: 1, name: 'John Smith' }], }); // navigate to the page await page.goto('/'); // assert page content according to mock await expect(page).toContainText('John Smith'); });
Check out MockClient
API for other methods.
-
Add a custom command
mockServerRequest
in support files, see example mock-server-request.js. -
Use the custom command to define mocks:
it('shows list of users', () => { // set up server-side mock cy.mockServerRequest('https://jsonplaceholder.typicode.com/users', { body: [{ id: 1, name: 'John Smith' }], }); // navigate to the page cy.visit('/'); // assert page content according to mock cy.get('li').first().should('have.text', 'John Smith'); });
You can integrate RMP with any test runner. It requires two steps:
- Use the
MockClient
class to define mocks. - Attach
mockClient.headers
to the navigation request.
On the server side, you should set up an interceptor to catch the requests and apply your mocks.
Add the following code to the top level layout.tsx
:
// app/layout.tsx
import { headers } from 'next/headers';
if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV !== 'production') {
const { setupFetchInterceptor } = await import('request-mocking-protocol/fetch');
setupFetchInterceptor(() => headers());
}
// ...
Note
Apply interceptor only in nodejs
runtime.
Important
Don't load interceptor inside instrumentation.ts, as it will be cleared in dev server after re-compilation.
See astro.config.ts in the astro-cypress example.
You can write an interceptor for any framework. It requires two steps:
- Read the HTTP headers of the incoming request.
- Capture outgoing HTTP requests.
Check out the reference implementations in the src/interceptors directory.
RMP offers flexible matching options to ensure your mocks are applied exactly when you need them:
-
Exact URL matching: Match requests by providing a full URL string.
await mockClient.GET('https://api.example.com/users', { body: [] });
-
Wildcard matching: Use wildcards with URLPattern-style syntax.
await mockClient.GET('https://api.example.com/users/*', { body: [] });
-
Regular expression matching: Match requests using JavaScript regular expressions.
await mockClient.GET(/\/users\/\d+$/, { body: {} });
-
Query parameter matching: Match specific query parameters for more targeted mocks.
await mockClient.GET({ url: 'https://api.example.com/users', query: { role: 'admin' }, }, { body: [] });
-
Method-based matching: Explicitly define the HTTP method (
GET
,POST
, etc.) to avoid accidental matches.await mockClient.POST('https://api.example.com/users', { status: 201 });
-
Schema matching: Use full request schemas to match by method, URL, query, and optionally enable
debug
mode for inspection.await mockClient.GET({ method: 'GET', url: 'https://api.example.com/users', query: { active: 'true' }, debug: true, }, { body: [] });
You can define route parameters in the URL pattern and use them in the response:
await mockClient.GET('https://jsonplaceholder.typicode.com/users/:id', {
body: {
id: '{{ id:number }}',
name: 'User {{ id }}',
}
});
The request:
GET https://jsonplaceholder.typicode.com/users/1
will be mocked with the response:
{
id: 1,
name: 'User 1',
}
Response patching allows to make a real request, but modify parts of the response for the testing purposes.
RMP supports response patching by providing the bodyPatch
key in the response schema:
await mockClient.GET('https://jsonplaceholder.typicode.com/users', {
bodyPatch: {
'[0].address.city': 'New York',
},
});
The final response will contain actual and modified data:
[
{
"id": 1,
"name": "Leanne Graham",
"address": {
- "city": "Gwenborough",
+ "city": "New York",
...
}
}
...
]
This technique is particularly useful to keep your tests in sync with actual API responses, while maintaining test stability and logic.
The bodyPatch
contains object in a form:
{
[path.to.property]: new value
}
path.to.property
uses dot-notation, evaluated with lodash.set.
You can debug the mocking process by providing debug: true
option to either request or response schema:
await mockClient.GET(
{
url: 'https://jsonplaceholder.typicode.com/users',
query: {
foo: 'bar',
},
debug: true, // <-- use debugging
},
{
body: [{ id: 1, name: 'John Smith' }],
},
);
When applying this mock, the server console with output the following:

The request schema is a serializable object that defines parameters for matching a request.
Example:
{
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/users',
query: {
foo: 'bar'
}
}
This schema will match the request:
GET https://jsonplaceholder.typicode.com/users?foo=bar
The response schema is a serializable object that defines how to build the mocked response.
Example:
{
status: 200,
body: 'Hello world'
}
Request-mocking-protocol uses a custom HTTP header x-mock-request
for transferring JSON-stringified schemas from the test runner to the application server.
Example:
x-mock-request: [{"reqSchema":{"method":"GET","patternType":"urlpattern","url":"https://example.com"},"resSchema":{"body":"hello","status":200}}]
On the server side, the interceptor will read the incoming headers and apply the mocks.
-
Static Data Only: The mock must be serializable to JSON. This means you can't provide arbitrary function-based mocks. To mitigate this restriction, RMP supports Parameter Substitution and Response Patching techniques.
-
Header Size Limits: HTTP headers typically support 4KB to 8KB of data. This approach is best suited for small payloads.
The MockClient
class is used on the test-runner side to define HTTP request mocks.
Creates a new instance of MockClient
.
options
(optional): An object containing configuration options.debug
(optional): A boolean indicating whether to enable debug mode.defaultMethod
(optional): The default HTTP method to use for requests.
Returns HTTP headers that are built from the mock schemas. Can be sent to the server for mocking server-side requests.
A callback function that is called whenever the mocks are changed.
Adds a new mock for the corresponding HTTP method.
-
reqSchema: string | RegExp | object
– The request schema for the mock.- If defined as
string
, it is treated as URLPattern for matching the request only by URL. - If defined as
RegExp
, it is treated as RegExp for matching the request only by URL.
- If defined as
-
resSchema: number | object
: The response schema for the mock.- If defined as
number
, it is treated as an HTTP status code.
- If defined as
Examples:
// mock any GET request to https://example.com
await mockServerRequest.GET('https://example.com/*', {
body: {
id: 1,
name: 'John Smith'
},
});
// mock any POST request to https://example.com having foo=bar in query
await mockServerRequest.POST({
url: 'https://example.com/*',
query: {
foo: 'bar'
},
}, {
body: {
id: 1,
name: 'John Smith'
},
});
Clears all mocks and rebuilds the headers.
Interceptors are used on the server to capture HTTP requests and apply mocks. Currently, there are two interceptors available.
This interceptor overwrites the globalThis.fetch
function.
Basic usage:
const { setupFetchInterceptor } = await import('request-mocking-protocol/fetch');
setupFetchInterceptor(() => {
// read and return headers of the incoming HTTP request
});
The actual function for retrieving incoming headers depends on the application framework.
If your app doesn’t use fetch
, you can try the MSW interceptor, which can capture a broader range of request types:
import { setupServer } from 'msw/node';
import { createHandler } from 'request-mocking-protocol/msw';
const mockHandler = createHandler(() => {
// read and return headers of the incoming HTTP request
});
const mswServer = setupServer(mockHandler);
mswServer.listen();
Note that MSW is used only to capture the request, while the mocks should be declaratively defined using the MockClient class.
The function for retrieving incoming HTTP headers depends on the application framework. Example for Next.js:
// app/layout.tsx
import { headers } from 'next/headers';
if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV !== 'production') {
const { setupServer } = await import('msw/node');
const { createHandler } = await import('request-mocking-protocol/msw');
const mockHandler = createHandler(() => headers());
const mswServer = setupServer(mockHandler);
mswServer.listen();
}
export default function RootLayout({ ... });
While both RMP and MSW support request mocking, RMP stands out by enabling per-test isolation and parallelization for server-side mocks. It also allows mocking server-side requests when tests run on CI against a remote target.
Feature | RMP | MSW |
---|---|---|
REST API | ✅ | ✅ |
GraphQL API | ❌ | ✅ |
Arbitrary handler function | ❌ | ✅ |
Server-side mocking | ✅ | ✅ |
Server-side mocking with per-test isolation | ✅ | ❌¹ |
Server-side mocking on CI | ✅ | ❌ |
¹ Per-test isolation in MSW can be achieved via spinning a separate app instance for each test. See this example.