Skip to content

Request Interceptor Mode

The Request Interceptor Mode provides a lightweight alternative to mock servers. Instead of creating a mock server with fixture responses, it intercepts real HTTP requests to record interactions while allowing your application to call actual APIs.

Interceptor Mode is perfect for:

  • Integration testing against real APIs
  • Development recording of actual API usage patterns
  • Contract testing with live services
  • Minimal setup scenarios where you don’t want to manage ports

Mock Server Mode is better for:

  • Unit testing with isolated, deterministic responses
  • Offline development when APIs aren’t available
  • Fixture-based testing with approved test data
FeatureMock Server ModeInterceptor Mode
PurposeIsolated testing with fixturesIntegration testing with real APIs
API CallsMocked responsesReal API responses
Port RequiredYesNo
Fixture UsageYesNo (just for operation matching)
RecordingYesYes
Works withAny HTTP clientfetch + http module clients
Cleanupmock.close()using statement

The recommended approach uses the using statement for automatic cleanup:

import { createClient } from '@entente/consumer'
const entente = await createClient({
serviceUrl: 'https://entente.company.com',
consumer: 'web-app',
consumerVersion: '1.0.0',
environment: 'test',
recordingEnabled: true
})
{
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// All HTTP requests are now intercepted and recorded
await fetch('https://api.example.com/orders/123') // Real API call
await axios.get('https://api.example.com/orders') // Also intercepted
} // Automatically unpatch and batch send interactions

For more control over the lifecycle:

const interceptor = await entente.patchRequests('order-service', '2.1.0')
try {
// Make API calls
await fetch('https://api.example.com/orders/123')
} finally {
// Always clean up
await interceptor.unpatch()
}
import { test, expect } from 'vitest'
test('should process order', async () => {
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// This calls real API and records interaction
const response = await fetch('https://api.example.com/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: 'widget', quantity: 5 })
})
expect(response.status).toBe(201)
// Check intercepted calls
const calls = interceptor.getInterceptedCalls()
expect(calls).toHaveLength(1)
expect(calls[0].operation).toBe('createOrder') // OpenAPI operation ID
})
import request from 'supertest'
import app from './app.js'
test('should get order', async () => {
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Supertest uses http module - also intercepted!
const response = await request(app)
.get('/orders/123')
.expect(200)
const stats = interceptor.getStats()
expect(stats.http).toBe(1) // HTTP module request intercepted
expect(stats.total).toBe(1)
})
import axios from 'axios'
test('should handle axios requests', async () => {
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Axios uses http module under the hood
const response = await axios.get('https://api.example.com/orders/123')
expect(response.status).toBe(200)
const calls = interceptor.getInterceptedCalls()
expect(calls[0].operation).toBe('getOrder')
expect(calls[0].request.method).toBe('GET')
expect(calls[0].response.status).toBe(200)
})
const interceptor = await entente.patchRequests('order-service', '2.1.0', {
// Recording options
recording: true, // Default: true
// Optional URL filter - only intercept specific URLs
filter: (url) => url.includes('api.example.com')
})

Filter which requests to intercept:

const interceptor = await entente.patchRequests('order-service', '2.1.0', {
filter: (url) => {
// Only intercept API calls
return url.includes('/api/') || url.includes('api.example.com')
}
})
// These will be intercepted
await fetch('https://api.example.com/orders')
await fetch('https://myapp.com/api/orders')
// These will NOT be intercepted
await fetch('https://analytics.example.com/track')
await fetch('https://myapp.com/health')

The interceptor performs the same sophisticated operation matching as mock server mode:

{
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Matches OpenAPI operation by method + path pattern
await fetch('https://api.example.com/orders/123')
// → operation: 'getOrder'
await fetch('https://api.example.com/orders', { method: 'POST', ... })
// → operation: 'createOrder'
}
{
using interceptor = await entente.patchRequests('user-service', '1.0.0')
// Parses GraphQL query to extract operation name
await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'mutation CreateUser($input: UserInput!) { createUser(input: $input) { id } }'
})
})
// → operation: 'createUser'
await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'query GetUser($id: ID!) { user(id: $id) { name email } }'
})
})
// → operation: 'GetUser'
}
const interceptor = await entente.patchRequests('order-service', '2.1.0')
// Make some requests
await fetch('https://api.example.com/orders/123')
await fetch('https://api.example.com/orders', { method: 'POST', ... })
// Inspect what was captured
const calls = interceptor.getInterceptedCalls()
console.log('Intercepted calls:', calls.length)
calls.forEach(call => {
console.log(`${call.request.method} ${call.request.url}`)
console.log(`→ Operation: ${call.operation}`)
console.log(`→ Status: ${call.response.status}`)
console.log(`→ Duration: ${call.duration}ms`)
})
const stats = interceptor.getStats()
console.log(`Fetch requests: ${stats.fetch}`)
console.log(`HTTP module requests: ${stats.http}`)
console.log(`Total requests: ${stats.total}`)
// Interactions are recorded for Entente service
const interactions = interceptor.getRecordedInteractions()
console.log(`Recorded ${interactions.length} interactions for upload`)

The interceptor works with any HTTP client built on standard Node.js APIs:

  • Native Node.js fetch (18+)
  • fetch polyfills
  • Direct fetch calls
  • axios - Popular HTTP client
  • got - Lightweight HTTP client
  • node-fetch - fetch for older Node versions
  • supertest - Express/Koa testing
  • request - Legacy HTTP client (deprecated)
  • Custom clients built on http/https modules
{
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// All of these are intercepted
await fetch('https://api.example.com/orders/1')
await axios.get('https://api.example.com/orders/2')
await got('https://api.example.com/orders/3')
const stats = interceptor.getStats()
expect(stats.total).toBe(3)
}
describe('Order Processing Integration', () => {
test('complete order workflow', async () => {
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Real API calls throughout the workflow
const order = await createOrder({ product: 'widget', quantity: 5 })
await updateOrderStatus(order.id, 'processing')
await addShippingInfo(order.id, { address: '123 Main St' })
const finalOrder = await getOrder(order.id)
expect(finalOrder.status).toBe('processing')
// Verify all operations were captured
const calls = interceptor.getInterceptedCalls()
expect(calls.map(c => c.operation)).toEqual([
'createOrder',
'updateOrder',
'updateOrder',
'getOrder'
])
})
})
// Record real usage patterns during development
async function recordDevelopmentSession() {
using interceptor = await entente.patchRequests('order-service', '2.1.0', {
recording: true,
filter: (url) => url.includes('api.company.com')
})
// Normal application flow
await runApplication()
// All API interactions automatically recorded
console.log(`Recorded ${interceptor.getInterceptedCalls().length} API calls`)
}
test('verify compatibility with order-service v2.1.0', async () => {
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Run your application against the real service
await processOrderWorkflow()
// Verify all operations were successfully matched
const calls = interceptor.getInterceptedCalls()
const unknownOperations = calls.filter(c => c.operation === 'unknown')
expect(unknownOperations).toHaveLength(0)
// Verify expected operations were called
const operations = calls.map(c => c.operation)
expect(operations).toContain('createOrder')
expect(operations).toContain('getOrder')
})

For scenarios where you want fixtures but not full mock servers:

// Download approved fixtures for a service version
const fixtures = await entente.downloadFixtures('order-service', '2.1.0')
console.log(`Downloaded ${fixtures.length} fixtures`)
// Use fixtures however you want:
// - Save to disk for offline testing
// - Use in custom mock implementations
// - Analyze for test coverage
// Save to local file
import { writeFileSync } from 'fs'
writeFileSync(
'./fixtures/order-service-v2.1.0.json',
JSON.stringify(fixtures, null, 2)
)

In CI environments, interactions are automatically recorded and sent to Entente:

const entente = await createClient({
serviceUrl: 'https://entente.company.com',
consumer: 'web-app',
consumerVersion: '1.0.0',
environment: 'test',
recordingEnabled: process.env.CI === 'true' // Only record in CI
})
{
using interceptor = await entente.patchRequests('order-service', '2.1.0')
// These interactions will be recorded in CI
await processOrders()
} // Batch sent to Entente when disposed
const interceptor = await entente.patchRequests('order-service', '2.1.0', {
recording: false // Disable recording
})
// Make requests without recording
await fetch('https://api.example.com/orders/123')
// Enable recording for specific calls
const recordingInterceptor = await entente.patchRequests('order-service', '2.1.0', {
recording: true
})

Under the hood, the interceptor uses @mswjs/interceptors to patch:

  • fetch API: Native fetch and fetch polyfills
  • http module: http.request, http.get, https.request, https.get
  • ClientRequest: Low-level HTTP client used by most libraries

This provides comprehensive coverage of HTTP clients in the Node.js ecosystem.

  • Mock Servers - Learn about the full mock server mode
  • GitHub Actions - Integrate interceptor mode into CI/CD
  • Fixtures - Understand fixture management and approval workflows
interface RequestInterceptor {
// Control
unpatch(): Promise<void>
isPatched(): boolean
// Inspection
getInterceptedCalls(): InterceptedCall[]
getRecordedInteractions(): ClientInteraction[]
// Statistics
getStats(): {
fetch: number // Number of fetch requests intercepted
http: number // Number of http module requests intercepted
total: number // Total requests intercepted
}
// Automatic cleanup
[Symbol.dispose](): Promise<void>
}
interface InterceptedCall {
request: {
method: string
url: string
headers: Record<string, string>
body: unknown
}
response: {
status: number
headers: Record<string, string>
body: unknown
}
// Operation matching results
operation: string // e.g., 'getOrder', 'createOrder'
matchContext: {
selectedOperationId: string
candidates: Array<{
operationId: string
confidence: number
reasons: string[]
}>
}
// Metadata
duration: number
timestamp: Date
consumer: string
consumerVersion: string
environment: string
}