npm.io
0.1.7 • Published 3d ago

@ferrimock/node

Licence
MIT
Version
0.1.7
Deps
0
Size
57 kB
Vulns
0
Weekly
0

Ferrimock

High-performance HTTP mocking engine for Node.js, powered by Rust. Drop-in replacement for MSW with 3-4x better performance.

Why Ferrimock?

  • 3-4x faster than MSW -- Rust mock matching engine + NAPI FunctionRef optimization
  • MSW drop-in API -- setupServer, http.get(), HttpResponse.json(), graphql.link(), server.use(), lifecycle events
  • Declarative mocks -- YAML/JSON/HAR files with Tera templates and 115+ fake data generators
  • Zero-config interceptor -- Patches fetch, XMLHttpRequest, and http.ClientRequest, works with any test runner

Performance

Mode Ferrimock MSW Speedup
Declarative (inline) 9us N/A Rust-only, no JS crossing
Template + fake data 8us N/A Rust Tera engine + fake generators
JS handler (static) 15us 37us 2.5x
JS handler + fake data 18us 46us 2.6x
Full interceptor flow 13us 35us 2.7x

Quick Start

bun add ferrimock
setupServer (MSW drop-in)
import { setupServer } from 'ferrimock/node'
import { http, HttpResponse, delay } from 'ferrimock'

const server = setupServer(
  http.get('/api/users/:id', async ({ params }) => {
    await delay(100)
    return HttpResponse.json({ id: params.id, name: 'John' })
  }),
)

server.listen({ onUnhandledRequest: 'error' })

// fetch is now intercepted
const res = await fetch('http://localhost/api/users/42')
const user = await res.json() // { id: '42', name: 'John' }

server.close()

Existing MSW test suites keep working: swap the msw / msw/node imports for ferrimock / ferrimock/node. Resolvers receive { request, params, cookies, requestId } with a real Fetch Request; returning undefined falls through to the next handler; { once: true }, generator resolvers, passthrough(), absolute-URL predicates, and server.boundary() all behave like MSW. setupWorker (browser service worker mode) is not provided — the engine is a native addon.

Declarative Mocks (YAML)
const interceptor = new FerrimockInterceptor()

await interceptor.loadMocks('./mocks')
interceptor.apply()
# mocks/users.yaml
mocks:
- id: get-user
  match:
    methods: ["GET"]
    url: "/api/users/:id"
  response:
    status: 200
    headers:
      content-type: "application/json"
    template: |
      {
        "id": "{{ captures.id }}",
        "name": "{{ fake_name() }}",
        "email": "{{ fake_email() }}"
      }
Scripted Mocks (QuickJS, no Node required)

.js/.mjs/.ts/.mts files in the mocks directory define MSW-style handlers that run on an embedded QuickJS engine — the CLI server and any Rust embedder execute them without Node. Files are bundled by rolldown (TypeScript transpiled, node_modules and relative imports resolved + tree-shaken), compiled once to QuickJS bytecode, and cached on disk keyed by toolchain ABI + transitive input hashes — warm starts skip bundling and compiling entirely. Error positions map back to the original sources. Handlers support async/await, delay(), fake.*, and module-scope state (counters, in-memory stores) that persists across requests and resets on hot reload.

// mocks/users.mjs
let hits = 0;

http.get('/api/users/:id', ({ params }) => {
  hits += 1;
  return HttpResponse.json({ id: params.id, name: fake.name(), hits });
});

http.post('/api/login', async () => {
  await delay(100);
  return HttpResponse.json({ token: fake.jwt() }, { status: 201 });
});
ferrimock mock serve mocks/   # picks up .js/.mjs next to YAML/JSON/HAR, hot reloads all

Portable with Node: import { http, HttpResponse, fake, delay } from 'ferrimock' works in both runtimes — the same file loads under the CLI (QuickJS) and under Node via loadMocksDir (V8), whether it registers with bare calls or export default [...]. RegExp paths (http.get(/^\/api\/\d+$/i, ...)), HttpResponse.error(), and passthrough() behave the same in both. npm packages resolve and bundle in both runtimes. Scripted handler calls cost ~10us (Rust matching + QuickJS execution); matching never touches JS. Enabled via the scripting cargo feature (included in full; excluded from the Node addon, where V8 runs the files instead).

HTTP Server Mode
import { FerrimockServer, http, HttpResponse } from 'ferrimock'

const server = new FerrimockServer()

server.useHandlers([
  http.get('/api/users/:id', async ({ params }) =>
    HttpResponse.json({ id: params.id, name: 'John' })
  ),
])

const url = await server.listen(3000)
// Server running at http://127.0.0.1:3000

await server.close()

MSW-Compatible API

HTTP Handlers
import { http, HttpResponse } from 'ferrimock'

http.get('/path', resolver)      // GET
http.post('/path', resolver)     // POST
http.put('/path', resolver)      // PUT
http.delete('/path', resolver)   // DELETE
http.patch('/path', resolver)    // PATCH
http.head('/path', resolver)     // HEAD
http.options('/path', resolver)  // OPTIONS
http.all('/path', resolver)      // Any method

// One-time handlers
http.get('/path', resolver, { once: true })

// RegExp matching
http.get(/^\/api\/users\/\d+$/, resolver)

// Absolute URLs (host + path matching)
http.get('https://api.example.com/users/:id', resolver)
GraphQL Handlers
import { graphql, HttpResponse } from 'ferrimock'

graphql.query('GetUser', ({ query, variables, operationName }) =>
  HttpResponse.json({ data: { id: variables.id } })
)
graphql.mutation('CreateUser', resolver)
graphql.mutation(/^Update/, resolver)   // RegExp operation names
graphql.operation(resolver)             // any operation

// Endpoint-scoped handlers
const github = graphql.link('https://api.github.com/graphql')
github.query('GetRepo', resolver)
Responses

HttpResponse extends the native Response — handlers can also return any plain Response.

HttpResponse.json({ key: 'value' })
HttpResponse.json({ key: 'value' }, { status: 201, headers: { 'x-custom': 'val' } })
HttpResponse.text('plain text')
HttpResponse.html('<h1>Hello</h1>')
HttpResponse.xml('<root/>')
HttpResponse.arrayBuffer(buffer)
HttpResponse.formData(formData)
HttpResponse.redirect('/target', 302)
HttpResponse.error()  // simulate network failure
new HttpResponse('body', { status: 418, statusText: "I'm a teapot" })
new HttpResponse(readableStream)  // streamed body; delivered live by the interceptor
new Response(null, { status: 204 })

Streamed bodies pass through the interceptor untouched — the caller reads the handler's own ReadableStream, chunk timing included. The standalone TCP server and the QuickJS runtime deliver the drained (buffered) body.

Resolver Info

Resolvers receive MSW's info object:

http.post('/api/users/:id', async ({ request, params, cookies, requestId }) => {
  request.url                      // full URL (real Fetch Request)
  request.method                   // 'POST'
  request.headers.get('accept')    // case-insensitive Headers
  await request.json()             // parsed body
  await request.text()             // raw body
  await request.formData()         // multipart or urlencoded body
  params.id                        // ':id' capture
  cookies.session                  // parsed request cookies
  requestId                        // matches lifecycle-event requestId
})

Returning undefined falls through to the next matching handler. Generator resolvers (function*) advance one yield per request, and the last value repeats after the generator is done.

Utilities
import { delay, passthrough, bypass } from 'ferrimock'

// Delay response
http.get('/api/slow', async () => {
  await delay(200)        // exact ms
  await delay('real')     // random 100-400ms
  await delay('infinite') // never resolves (test timeouts)
  return HttpResponse.json({ ok: true })
})

// Passthrough to real network
http.get('/api/real', () => passthrough())

// Bypass interception for a specific request
const realResponse = await fetch(bypass('http://real-api.com/data'))
Server Methods
const server = setupServer(...handlers)

server.listen({ onUnhandledRequest: 'warn' }) // 'bypass' | 'warn' | 'error' | callback
server.use(...handlers)          // Runtime overrides (higher priority)
server.resetHandlers()           // Drop runtime overrides, restore initial handlers
server.resetHandlers(...next)    // Replace the entire handler set
server.restoreHandlers()         // Re-enable consumed { once } handlers
server.listHandlers()            // List active handlers
server.boundary(callback)        // Scoped handler isolation
server.close()                   // Restore fetch/XHR/ClientRequest
Lifecycle Events
server.events.on('request:start', ({ request, requestId }) => { ... })
server.events.on('request:match', ({ request, requestId }) => { ... })
server.events.on('request:unhandled', ({ request, requestId }) => { ... })
server.events.on('request:end', ({ request, requestId }) => { ... })
server.events.on('response:mocked', ({ request, requestId, response }) => { ... })
server.events.on('response:bypass', ({ request, requestId, response }) => { ... })
server.events.on('unhandledException', ({ request, requestId, error }) => { ... })
Fake Data (115+ generators)
import { fake } from 'ferrimock'

fake.uuid()         // '550e8400-e29b-41d4-a716-446655440000'
fake.name()         // 'John Smith'
fake.email()        // 'john@example.com'
fake.phone()        // '+1-555-0123'
fake.city()         // 'San Francisco'
fake.url()          // 'https://example.com'
fake.ipv4()         // '192.168.1.1'
fake.creditCard()   // '4111111111111111'
fake.jwt()          // 'eyJhbGciOiJIUzI1NiJ9...'
fake.sentence()     // 'The quick brown fox...'
// ... 100+ more

Packages

Package Description
ferrimock (npm) The MSW drop-in surface (ferrimock + ferrimock/node), alias of ferrimock
ferrimock setupServer, interceptor, HttpResponse, config loader
@ferrimock/node Rust NAPI bindings (http, graphql, HttpResponse builders, fake, FerrimockServer)
@ferrimock/playwright Playwright fixture adapter

Rust Library

Ferrimock is also a standalone Rust library for mock matching, template rendering, and HTTP server.

[dependencies]
ferrimock = { git = "https://github.com/salamaashoush/ferrimock", features = ["full"] }

See Mock Engine, Fake Data, GraphQL, CLI Reference.

CLI

npm install -g @ferrimock/cli              # Prebuilt binary via npm
cargo install ferrimock-cli --locked       # Or build from source

ferrimock mock serve mocks/                # Serve mocks with hot reload
ferrimock mock create "/api/users/:id"     # Create a mock
ferrimock mock test -m GET /api/users/123  # Test matching
ferrimock fake data email --count 10       # Generate fake data

License

MIT OR Apache-2.0

Keywords