npm package: @klogt/intercept
Description: The lightweight HTTP interception library built for modern Node.js testing. MSW-inspired simplicity meets native `fetch` performance.
Author: Klogt
Latest version: 2.0.2

<div align="center">

# @klogt/intercept

**Stop writing mocks. Start declaring routes.**

The lightweight HTTP interception library built for modern Node.js testing.  
MSW-inspired simplicity meets native `fetch` performance.

[![npm version](https://img.shields.io/npm/v/@klogt/intercept.svg)](https://www.npmjs.com/package/@klogt/intercept)
[![npm downloads](https://img.shields.io/npm/dm/@klogt/intercept.svg)](https://www.npmjs.com/package/@klogt/intercept)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-20+-green.svg)](https://nodejs.org/)
[![CI](https://github.com/klogt-as/intercept/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/klogt-as/intercept/actions/workflows/build-and-test.yml)

</div>

---

## Why Choose @klogt/intercept?

**Testing APIs shouldn't be hard.** You shouldn't need to:
- Mock individual functions for every network call
- Set up complex test servers
- Learn heavyweight frameworks when you just need simple interception
- Wait for slow, flaky tests

With `@klogt/intercept`, you declare routes **right in your tests** — exactly like you think about them.

```ts
// That's it. No setup files, no complex config.
intercept.get("/users").resolve([{ id: 1, name: "Ada" }]);
```

### Built for Modern JavaScript

✨ **Native Node 20+ `fetch`** — no polyfills, no patches, just works  
🎯 **Zero dependencies** — only 2.6kb gzipped, installs in milliseconds  
🔧 **Works with your stack** — Vitest, Jest, React Testing Library, TanStack Query
🚀 **Millisecond setup** — one file, three lines, done  
📦 **TypeScript-first** — full type inference, no `any` in sight  
⚡ **Lightning fast** — tests run at native speed

### Perfect For

- 🧪 **Frontend developers** testing React, Vue, or Svelte apps
- 🏗️ **Integration tests** that need predictable API responses
- 🔄 **TanStack Query** users who want cleaner test mocks
- 📱 **Component testing** with React Testing Library
- ⚙️ **CI/CD pipelines** where speed matters

---

## Quick Example

```ts
import { intercept } from "@klogt/intercept";

it("fetches and displays users", async () => {
  // Start intercepting with your API origin
  intercept.listen({
    origin: 'https://api.example.com',
    onUnhandledRequest: 'error'
  });

  // Declare what the API should return
  intercept.get("/users").resolve([
    { id: 1, name: "Ada Lovelace" },
    { id: 2, name: "Grace Hopper" }
  ]);

  // Run your component/code
  render(<UserList />);

  // Assert the results
  expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument();
  expect(await screen.findByText("Grace Hopper")).toBeInTheDocument();
});
```

No mocks. No spies. No complexity. Just declare routes and test.

---

## Features That Matter

🎯 **Inline route declarations** — define mocks exactly where you need them  
📝 **Smart defaults** — `POST` returns 201, `DELETE` returns 204, etc.  
🔍 **Path parameters** — `/users/:id` just works  
⏱️ **Delay simulation** — test loading states with `.delay(500)`  
🚨 **Error scenarios** — `.reject({ status: 500 })` for error testing  
🔄 **Works with Axios** — optional adapter for non-fetch clients  
🧹 **Test isolation** — `intercept.reset()` between tests  
🛠️ **Unhandled request strategies** — warn, bypass, or error on unexpected calls

---

## Table of Contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start (Vitest)](#quick-start-vitest)
- [Understanding Origins](#understanding-origins)
- [Using with React + TanStack Query](#using-with-react--tanstack-query)
- [Defining routes](#defining-routes)
- [Ignoring requests](#ignoring-requests)
- [Unhandled requests](#unhandled-requests)
- [Axios adapter (optional)](#axios-adapter-optional)
- [Common Testing Patterns](#common-testing-patterns)
- [API reference](#api-reference)
- [Troubleshooting](#troubleshooting)
- [Comparison with MSW](#comparison-with-msw)
- [Contributing](#contributing)
- [License](#license)

---

## Requirements

- **Node 20+** (uses built-in `fetch`)
- Test runner: **Vitest** or **Jest**

---

## Installation

### npm
```bash
npm i -D @klogt/intercept
```
### pnpm
```bash
pnpm add -D @klogt/intercept
```
### yarn
```bash
yarn add -D @klogt/intercept
```

No need to install `axios` unless you plan to attach the Axios adapter.

---

## Quick start (Vitest)

### Getting started: Inline approach

The simplest way to get started is to call `intercept.listen()` directly in your tests:

```ts
import { intercept } from "@klogt/intercept";

it("fetches users from API", async () => {
  // Start intercepting with your API origin
  intercept.listen({
    origin: 'https://api.example.com',
    onUnhandledRequest: 'error'
  });

  // Declare what the API should return
  intercept.get("/users").resolve([
    { id: 1, name: "Ada" },
    { id: 2, name: "Grace" }
  ]);

  // Your code calls fetch('/users') - it gets the mocked response
  const res = await fetch('/users');
  const users = await res.json();
  
  expect(users).toHaveLength(2);
  expect(users[0].name).toBe("Ada");
});
```

This works great for quick tests! However, you'll likely want to extract the setup to avoid repetition.

**Note**: `onUnhandledRequest` defaults to `'error'` in test environments (Vitest/Jest) and `'warn'` otherwise, so you can omit it if the default works for you.

### Setup Option 1: Using setupIntercept() helper

For minimal boilerplate, use the `setupIntercept()` helper in a shared setup file:

```ts
// setupTests.ts
import { setupIntercept } from "@klogt/intercept";

setupIntercept({
  origin: 'https://api.example.com',
  onUnhandledRequest: 'error'
});
```

This automatically registers `beforeAll`, `afterEach`, and `afterAll` hooks for you. Then configure this file in your `vitest.config.ts`:

```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    setupFiles: ['./setupTests.ts'],
  },
});
```

Now your tests can be clean and focused:

```ts
import { intercept } from "@klogt/intercept";

it("fetches users from API", async () => {
  // Just declare routes - setup is handled automatically
  intercept.get("/users").resolve([
    { id: 1, name: "Ada" },
    { id: 2, name: "Grace" }
  ]);

  const res = await fetch('/users');
  const users = await res.json();
  
  expect(users).toHaveLength(2);
});
```

### Alternative: Per-test suite setup

If you prefer to keep setup within your test files, you can use `describe` blocks with lifecycle hooks:

```ts
import { intercept } from "@klogt/intercept";

describe("User API", () => {
  beforeAll(() => {
    intercept.listen({
      origin: 'https://api.example.com',
      onUnhandledRequest: 'error'
    });
  });

  afterEach(() => {
    intercept.reset();
  });

  afterAll(() => {
    intercept.close();
  });

  it("fetches users", async () => {
    intercept.get("/users").resolve([
      { id: 1, name: "Ada" },
      { id: 2, name: "Grace" }
    ]);

    const res = await fetch('/users');
    const users = await res.json();
    
    expect(users).toHaveLength(2);
  });

  it("handles errors", async () => {
    intercept.get("/users").reject({ status: 500 });
    
    await expect(fetch('/users')).rejects.toThrow();
  });
});
```

This approach gives you control over the interceptor lifecycle per test suite without needing a shared setup file.

### Setup Option 2: Shared setup file (manual approach)

For maximum control (custom logging, conditional setup, etc.), you can manually manage the lifecycle hooks:

```ts
// setupTests.ts
import { intercept } from "@klogt/intercept";

beforeAll(() => {
  intercept.listen({
    origin: 'https://api.example.com',
    onUnhandledRequest: 'error'
  });
});

afterEach(() => {
  intercept.reset();
});

afterAll(() => {
  intercept.close();
});
```

**Note**: This is what `setupIntercept()` does for you under the hood. Use this manual approach when you need to add custom logic to the lifecycle hooks.

Configure this file in your `vitest.config.ts`:

```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    setupFiles: ['./setupTests.ts'],
  },
});
```

---

## Understanding Origins

When you use **relative paths** like `/users`, you need to set a base URL:

```ts
intercept.listen({ 
  origin: 'https://api.example.com',
  onUnhandledRequest: 'error'
});

intercept.get("/users").resolve([{ id: 1 }]);
// Matches: https://api.example.com/users
```

**Absolute URLs** bypass the origin and match exactly:

```ts
intercept.get("https://payments.stripe.com/v1/charges").resolve({ id: "ch_123" });
// Matches exactly - ignores origin setting
```

You can mix relative and absolute URLs in the same test as needed.

---

## Using with React + TanStack Query

`@klogt/intercept` plays nicely with data fetching libraries like **TanStack Query**.

Here's a simple example component:

```tsx
// Users.tsx
import { useQuery } from "@tanstack/react-query";

export function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["users"],
    queryFn: async () => {
      const res = await fetch("/users");
      if (!res.ok) throw new Error("Failed to fetch users");
      return res.json();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {data.map((u: { id: number; name: string }) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
```

### Test with intercept

```tsx
// Users.test.tsx
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Users } from "./Users";
import { intercept } from "@klogt/intercept";

function renderWithQuery(ui: React.ReactElement) {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });
  return render(
    <QueryClientProvider client={queryClient}>
      {ui}
    </QueryClientProvider>
  );
}

it("renders users from mocked API", async () => {
  intercept.get("/users").resolve([
    { id: 1, name: "Ada" },
    { id: 2, name: "Grace" },
  ]);

  renderWithQuery(<Users />);

  expect(await screen.findByText("Ada")).toBeInTheDocument();
  expect(await screen.findByText("Grace")).toBeInTheDocument();
});

it("shows error state on API failure", async () => {
  intercept.get("/users").reject({
    status: 500,
    body: { message: "Server error" }
  });

  renderWithQuery(<Users />);

  expect(await screen.findByText(/Error:/)).toBeInTheDocument();
});
```

---

## Defining routes

### Basic responses

```ts
// GET /users → 200 with array
intercept.get("/users").resolve([{ id: 1, name: "Ada" }]);

// POST /users → 201 with body
intercept.post("/users").resolve({ id: 2, name: "Grace" });

// DELETE /users/2 → 204 (no content)
intercept.delete("/users/:id").resolve(null, { status: 204 });

// PUT with custom status and headers
intercept.put("/profile").resolve(
  { id: "me", updated: true },
  {
    status: 200,
    headers: { "x-request-id": "abc123" },
  }
);
```

### Error responses

```ts
// Reject with error status and body
intercept.post("/login").reject({
  status: 401,
  body: { code: "INVALID_CREDENTIALS", message: "Invalid username or password" }
});

// 404 Not Found
intercept.get("/users/:id").reject({
  status: 404,
  body: { error: "User not found" }
});

// 422 Validation Error
intercept.post("/users").reject({
  status: 422,
  body: {
    errors: [
      { field: "email", message: "Email is required" }
    ]
  }
});
```

### Path parameters

```ts
// Match dynamic segments with :param
intercept.get("/users/:id").resolve({ id: "123", name: "Ada" });

// Access params in dynamic resolver
intercept.get("/users/:id").handle(({ params }) => {
  return Response.json({
    id: params.id,
    name: `User ${params.id}`
  });
});

// Multiple params
intercept.get("/orgs/:orgId/repos/:repoId").handle(({ params }) => {
  return Response.json({
    org: params.orgId,
    repo: params.repoId
  });
});

// Catch-all wildcard
intercept.get("/*").resolve({ message: "Catch all requests" });
```

### Adding delays with `.delay()`

Simulate network latency to test loading states:

```ts
// Resolve after 500ms
intercept.get("/users").delay(500).resolve([{ id: 1, name: "Ada" }]);

// Reject after 1 second
intercept.post("/login").delay(1000).reject({
  status: 401,
  body: { error: "Timeout" }
});
```

### Dynamic resolvers with `.handle()`

Need to compute a response based on the incoming request? Use `.handle()` for full control:

```ts
intercept.post("/login").handle(async ({ request, body, params }) => {
  // body is parsed JSON (best-effort)
  if (body?.username === "admin" && body?.password === "secret") {
    return Response.json({ token: "abc123", user: { id: 1, name: "Admin" } });
  }
  
  return Response.json(
    { error: "Invalid credentials" },
    { status: 401 }
  );
});

// Access request headers
intercept.get("/protected").handle(({ request }) => {
  const auth = request.headers.get("Authorization");
  
  if (!auth) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  
  return Response.json({ data: "secret data" });
});

// Use path params
intercept.delete("/users/:id").handle(({ params }) => {
  console.log(`Deleting user ${params.id}`);
  return new Response(null, { status: 204 });
});
```

---

## Ignoring requests

Ignore requests to given paths across **all HTTP methods**. Handy for analytics, health checks, or any traffic you don't want your tests to care about. Returns **204 No Content** immediately to prevent test failures.

```ts
intercept.ignore(paths: ReadonlyArray<Path>)
```

**Example:**
```ts
// In setupTests.ts or beforeEach
intercept.ignore(['/analytics', '/ping', '/health-check']);

// All these return 204 immediately:
fetch('/analytics');           // GET
fetch('/ping', { method: 'POST' });  // POST
fetch('/health-check');        // any method
```

**Common use cases:**

```ts
// Ignore analytics and monitoring
intercept.ignore([
  '/api/analytics/*',
  '/api/metrics',
  '/api/telemetry'
]);

// Ignore third-party health checks
intercept.ignore([
  '/health',
  '/ready',
  '/livez'
]);
```

---

## Unhandled requests

Configure once when starting the server:

```ts
intercept.listen({ onUnhandledRequest: "error" });
```

### Strategies

- **`"error"`** (recommended for tests): Blocks unhandled requests with 501 status and logs an error
  ```
  [@klogt/intercept] ❌ Unhandled request (error mode)
     → GET https://api.example.com/unknown
  
  No intercept handler matched this request.
  The request was blocked with a 501 response.
  Tip: add one with:
     intercept.get('/unknown').resolve(...)
  ```

- **`"warn"`**: Logs a warning but allows the request to pass through to the real transport
  ```
  [@klogt/intercept] 🚧 Unhandled request
     → GET https://api.example.com/unknown
  
  No intercept handler matched this request.
  Tip: add one with:
     intercept.get('/unknown').resolve(...)
  ```

- **`"bypass"`**: Silently allows requests to pass through (no logging)

- **Function**: Decide dynamically per request
  ```ts
  intercept.listen({
    onUnhandledRequest: ({ request, url }) => {
      // Ignore OPTIONS requests
      if (request.method === 'OPTIONS') return 'bypass';
      
      // Warn about others
      return 'warn';
    }
  });
  ```

---

## Axios adapter (optional)

`@klogt/intercept` can intercept both `fetch` **and** Axios. Simply pass your axios instance to `intercept.listen()`:

```ts
import axios from "axios";

const apiClient = axios.create({
  baseURL: "https://api.example.com"
});

// In setupTests.ts
intercept.listen({
  onUnhandledRequest: 'error',
  adapter: apiClient  // Automatically wrapped and attached
});
```

**No runtime axios dependency**: the adapter's types reference `axios` conditionally so your library/app doesn't pull axios unless you install it yourself.

**How it works**: When you pass an axios instance to the `adapter` option, intercept automatically wraps it with an internal adapter that makes your axios requests go through the same route handlers as fetch requests.

---

## Common Testing Patterns

### Baseline routes with per-test overrides

```ts
// setupTests.ts
beforeAll(() => {
  intercept
    .listen({ onUnhandledRequest: 'error' })
    .origin('https://api.example.com');
    
  // Baseline routes that apply to all tests
  intercept.get("/config").resolve({ version: "1.0" });
  intercept.get("/health").resolve({ status: "ok" });
});

// users.test.ts
it("fetches users", () => {
  // Override or add specific routes for this test
  intercept.get("/users").resolve([{ id: 1 }]);
  // ... test code
});
```

### Testing error scenarios

```ts
it("handles network errors gracefully", async () => {
  intercept.get("/users").reject({
    status: 500,
    body: { error: "Internal server error" }
  });

  const { result } = renderHook(() => useUsers(), {
    wrapper: QueryClientProvider
  });

  await waitFor(() => {
    expect(result.current.error).toBeTruthy();
  });
});
```

### Ignoring non-essential requests

```ts
beforeEach(() => {
  // Ignore analytics so they don't cause test failures
  intercept.ignore([
    '/api/analytics/*',
    '/api/tracking',
    '/health'
  ]);
});
```

---

## API reference

### `intercept.listen(options)`

Start intercepting. Must be called before defining routes.

```ts
intercept.listen({
  origin?: string;
  onUnhandledRequest?: "warn" | "bypass" | "error" | ((args) => Strategy);
  adapter?: unknown;
});
```

**Parameters:**
- `origin`: (optional) Base URL for relative paths. Can also be set later via `.origin()`
- `onUnhandledRequest`: (optional) Strategy for unhandled requests. Defaults to `'error'` in test environments (Vitest/Jest) and `'warn'` otherwise
- `adapter`: (optional) Axios instance to intercept (see [Axios adapter section](#axios-adapter-optional))

Returns `intercept` for chaining with `.origin()`.

### `intercept.origin(url)`

Set the base URL for relative paths. Can be called in `beforeAll` (applies to whole file) or `beforeEach` (per-test).

```ts
intercept.origin('https://api.example.com');
```

Returns `intercept` for chaining.

### `intercept.<method>(path)`

Create a route for `GET | POST | PUT | PATCH | DELETE | OPTIONS`.

```ts
intercept.get(path: Path)
intercept.post(path: Path)
intercept.put(path: Path)
intercept.patch(path: Path)
intercept.delete(path: Path)
intercept.options(path: Path)
```

Each returns an object with:

#### `.delay(ms)`

Add a delay before responding. Returns a chainable object with resolve/reject/handle methods:

```ts
delay(ms: number): {
  resolve<T>(data: T, init?: ResolveInit): void;
  reject<T>(opts?: RejectInit<T>): void;
  handle<TRequest>(resolver: DynamicResolver<TRequest>): void;
}
```

**Example:**
```ts
intercept.get("/users").delay(500).resolve([{ id: 1 }]);
intercept.post("/login").delay(1000).reject({ status: 401 });
```

#### `.resolve(data, init?)`

Return successful JSON response:

```ts
resolve<T>(data: T, init?: {
  status?: number;      // Default: method-specific (200, 201, 204)
  headers?: Record<string, string>;
}): void;
```

If `status` is `204`, body is stripped automatically.

#### `.reject(opts?)`

Return error response:

```ts
reject<T>(opts?: {
  status?: number;      // Default: 400
  body?: T | undefined; // Optional error body
  headers?: Record<string, string>;
}): void;
```

#### `.fetching(init?)`

Simulate pending/loading state:

```ts
fetching(init?: {
  delayMs?: number;     // Undefined = hang forever
  status?: number;      // Default: 204
  headers?: Record<string, string>;
}): void;
```

#### `.handle(resolver)`

Full control with custom logic:

```ts
handle<TRequest>(
  resolver: (args: {
    request: Request;
    url: URL;
    params: Record<string, string>;
    body: TRequest | undefined;  // Parsed JSON, best-effort
  }) => Response | Promise<Response>
): void;
```

### `intercept.ignore(paths)`

Ignore requests to given paths across all HTTP methods:

```ts
intercept.ignore(paths: ReadonlyArray<Path>): void;
```

Returns 204 No Content immediately.

### `intercept.reset()`

Clear all registered handlers. Use in `afterEach` for test isolation:

```ts
afterEach(() => {
  intercept.reset();
});
```

### `intercept.close()`

Stop intercepting, detach adapters, restore globals, and clear all state. Use in `afterAll`:

```ts
afterAll(() => {
  intercept.close();
});
```

### `setupIntercept(options)`

Convenience helper that automatically registers lifecycle hooks for minimal boilerplate. Use this when you don't need custom logic in your hooks.

```ts
setupIntercept(options?: {
  origin?: string;
  onUnhandledRequest?: "warn" | "bypass" | "error" | ((args) => Strategy);
  adapter?: unknown;
}): void;
```

**Parameters:**
- `options`: (optional) Configuration object with the same options as `intercept.listen()`:
  - `origin`: Base URL for relative paths
  - `onUnhandledRequest`: Strategy for unhandled requests (default: auto-detected)
  - `adapter`: Axios instance to intercept

**What it does:**
- Automatically calls `intercept.listen(options)` in a `beforeAll` hook
- Automatically calls `intercept.reset()` in an `afterEach` hook
- Automatically calls `intercept.close()` in an `afterAll` hook

**Example:**
```ts
// setupTests.ts
import { setupIntercept } from "@klogt/intercept";

setupIntercept({
  origin: 'https://api.example.com',
  onUnhandledRequest: 'error'
});
```

**When to use:**
- You have a single API origin for your entire test suite
- You don't need custom logic in lifecycle hooks
- You want minimal boilerplate

**When to use manual setup instead:**
- You need custom logging or debugging in hooks
- You need conditional or dynamic setup
- You need to integrate with other test setup/teardown
- You need per-test origins

---

## Troubleshooting

### "No intercept handler matched this request"

This error means you tried to make a request that doesn't have a matching route. The error message now includes helpful context:

```
[@klogt/intercept] ❌ Unhandled request (error mode)
   → GET https://api.example.com/user

No intercept handler matched this request.
The request was blocked with a 501 response.

Registered handlers:
  GET /users
  POST /users
  GET /profile

Did you mean: GET /users?
```

**Common causes:**

1. **Forgot to set origin** for relative paths:
   ```ts
   // ❌ This won't work
   intercept.get("/users").resolve([...]);
   await fetch('/users');  // Error: no handler matched
   
   // ✅ Set origin first
   intercept.origin('https://api.example.com');
   intercept.get("/users").resolve([...]);
   await fetch('/users');  // Works!
   ```

2. **Path mismatch** (typo or wrong params):
   ```ts
   intercept.get("/users").resolve([...]);
   await fetch('/user');  // ❌ Typo - check "Did you mean?" suggestion
   ```

3. **Method mismatch**:
   ```ts
   intercept.get("/users").resolve([...]);
   await fetch('/users', { method: 'POST' });  // ❌ Handler is for GET
   ```

**Tip**: The error message now shows all registered handlers and may suggest the closest match to help you spot typos quickly!

### "Cannot find module 'axios' or its type declarations"

You don't need axios unless you attach the adapter. If you see this error:

1. Either install axios: `npm i axios`
2. Or don't import from `@klogt/intercept/axios`

The axios adapter uses conditional type imports to avoid a hard dependency.

### Tests are flaky or handlers leak between tests

Always call `intercept.reset()` in `afterEach`:

```ts
afterEach(() => {
  intercept.reset();
});
```

This ensures each test starts with a clean slate.

### Relative paths don't work

Make sure you've called `.origin()`:

```ts
// In setupTests.ts or beforeAll
intercept
  .listen({ onUnhandledRequest: 'error' })
  .origin('https://api.example.com');
```

Or use absolute URLs:

```ts
intercept.get("https://api.example.com/users").resolve([...]);
```

---

## Comparison with MSW

If you're familiar with [MSW](https://mswjs.io/), here's how `@klogt/intercept` compares:

| Feature | @klogt/intercept | MSW |
|---------|------------------|-----|
| **Setup complexity** | Minimal - one setup file | Requires worker setup |
| **Native fetch support** | Built-in (Node 20+) | Via msw/node |
| **Route definition** | Inline in tests | Inline or separate handlers |
| **Path params** | `:param` syntax | `:param` syntax |
| **TypeScript** | First-class, no `any` | Good support |
| **Browser support** | No (test-only) | Yes |
| **Bundle size** | Small | Larger |
| **Maturity** | New | Established |

**Choose @klogt/intercept if:**
- You only need test interception (not browser)
- You want minimal setup
- You prefer Node 20+ native features
- You want a lightweight package

**Choose MSW if:**
- You need browser support (dev mode mocking)
- You want battle-tested stability
- You need advanced features like streaming

---

## Contributing

Contributions are welcome! Please open an issue or PR on [GitHub](https://github.com/klogt-as/intercept).

---

## License

MIT © [Klogt](https://github.com/klogt-as)