Skip to main content

Getting started

This guide will help you get started with @zimic/interceptor.

Requirements

Supported environments

Client side

@zimic/interceptor is designed to work in any environment that supports the Fetch API. This includes any relatively modern browser. Can I Use is a great resource to check the compatibility of specific features with different browsers.

Server side

RuntimeVersion
Node.js>= 18.13.0

Supported languages

TypeScript

@zimic/interceptor requires TypeScript >= 5.0.

We recommend enabling strict in your tsconfig.json:

tsconfig.json
{
"compilerOptions": {
"strict": true
}
}

JavaScript

@zimic/interceptor is fully functional on JavaScript, although the type features will be disabled. Consider using TypeScript for improved type safety and developer experience.

Installation

@zimic/interceptor is available as a library on npm.

npm install @zimic/http @zimic/interceptor --save-dev
info

Note that @zimic/http is a peer dependency of @zimic/interceptor, so you need to install both packages. When upgrading @zimic/interceptor to a new version, consider upgrading @zimic/http as well to ensure that the versions are compatible.

We also have canary releases under the tag canary. These have the latest code, including new features, bug fixes, and possibly unstable or breaking changes.

npm install @zimic/http@canary @zimic/interceptor@canary --save-dev

Your first HTTP interceptor

Declaring an HTTP schema

To start using @zimic/interceptor, declare an HTTP schema using @zimic/http. The schema represents the structure of your API, including the paths, methods, request and response types.

TIP: OpenAPI Typegen

For APIs with an OpenAPI documentation (e.g. Swagger), the zimic-http typegen CLI can automatically infer the types and generate the schema for you. This is a great way to keep your schema is up to date and save time on manual type definitions.

schema.ts
import { HttpSchema } from '@zimic/http';

interface User {
username: string;
}

interface RequestError {
code: string;
message: string;
}

type Schema = HttpSchema<{
'/users': {
POST: {
request: { body: User };
response: {
201: { body: User };
400: { body: RequestError };
409: { body: RequestError };
};
};

GET: {
request: {
headers: { authorization: string };
searchParams: {
query?: string;
limit?: number;
};
};
response: {
200: { body: User[] };
400: { body: RequestError };
401: { body: RequestError };
};
};
};

'/users/:userId': {
PATCH: {
request: {
headers: { authorization: string };
body: Partial<User>;
};
response: {
204: {};
400: { body: RequestError };
};
};
};
}>;

Creating an HTTP interceptor

With the schema defined, you can now create an HTTP interceptor.

@zimic/interceptor provides a createHttpInterceptor function that takes the schema as a type parameter and returns an interceptor instance. It allows you to intercept HTTP requests, validate their contents, and return mock responses, all automatically typed based on the schema. The baseURL option represents the scope of the interceptor and points to the URL that your application will use to make requests.

import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
baseURL: 'http://localhost:3000',
});

You can also set other options, such as the interceptor type and how unhandled requests should be treated. Refer to the createHttpInterceptor API reference for more details.

INFO: Choosing your interceptor type

HTTP interceptors are available in two types: local (default) and remote.

import { createHttpInterceptor } from '@zimic/interceptor/http';

const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
});

When an interceptor is local, Zimic uses MSW to intercept requests in the same process as your application. This is the simplest way to start mocking requests and does not require any server setup.

Interceptors with type remote use a dedicated interceptor server to handle requests. This opens up more possibilities for mocking, such as handling requests from multiple applications. It is also more robust because it uses a regular HTTP server and does not depend on local interception algorithms.

Learn more about local interceptors and remote interceptors to see which one fits your needs. We recommend starting with local interceptors, as they are easier to get started with, and moving to remote interceptors if your use case is best served by them.

HTTP interceptor lifecycle

Starting an interceptor

To intercept requests, an interceptor must be started with interceptor.start(). This is usually done in a beforeAll hook in your test suite.

beforeAll(async () => {
await interceptor.start();
});
INFO: Local interceptors in browsers

If you are using a local interceptor in a browser environment, you must first initialize a mock service worker in your public directory before starting the interceptor.

INFO: Remote interceptors and interceptor servers

If you are using a remote interceptor, the baseURL should point to a running interceptor server, which is configured by the interceptor to handle requests. Learn more about starting remote HTTP interceptors.

Clearing an interceptor

When using an interceptor in tests, it's important to clear it between tests to avoid that one test affects another. This is performed with interceptor.clear(), which resets the interceptor and handlers to their initial states.

beforeEach(() => {
interceptor.clear();
});

Checking expectations

After each test, you can check if your application has made all of the expected requests with interceptor.checkTimes(). Learn more about how interceptors support declarative assertions to keep your tests clean and readable.

afterEach(() => {
interceptor.checkTimes();
});

Stopping an interceptor

After the interceptor is no longer needed, such as at the end of your test suite, you can stop it with interceptor.stop().

afterAll(async () => {
await interceptor.stop();
});

Mocking requests

You can now use the interceptor to handle requests and return mock responses. All paths, methods, parameters, requests, and responses are typed by default based on the schema.

test('example', async () => {
const users: User[] = [{ username: 'me' }];

interceptor
.get('/users')
.with({
headers: { authorization: 'Bearer my-token' },
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: users,
})
.times(1);

// Run the application and make requests...
});
INFO: Remote interceptors are asynchronous

Many operations in remote interceptors are asynchronous because they may involve communication with an interceptor server. This is different from local interceptors, which have mostly synchronous operations.

If you need to access the requests processed by the interceptor, use handler.requests.

const handler = interceptor
.get('/users')
.with({
headers: { authorization: 'Bearer my-token' },
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: users,
})
.times(1);

// Run the application and make requests...

console.log(handler.requests); // 1

console.log(handler.requests[0].headers.get('authorization')); // 'Bearer my-token'

console.log(handler.requests[0].searchParams.size); // 1
console.log(handler.requests[0].searchParams.get('username')); // 'my'

console.log(handler.requests[0].body); // null

Next steps

Guides

Take a look at our guides for more information on how to use @zimic/interceptor in common scenarios.

Examples

Try our examples for more practical use cases of @zimic/interceptor with popular libraries and frameworks.

API reference

Visit the API reference for more details on the resources available in @zimic/interceptor.

Explore the ecosystem

  • Access the @zimic/http documentation to learn more about extending your HTTP schema.

  • If you are interested in improving the type safety and development experience of your application code, check out @zimic/fetch. Use the same schema as your interceptor to automatically type your paths, methods, requests, parameters, and responses in a minimal fetch-like API client.