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
Runtime | Version |
---|---|
Node.js | >= 18.13.0 |
Supported languages
TypeScript
@zimic/interceptor
requires TypeScript >= 5.0.
We recommend enabling strict
in your 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
- pnpm
- yarn
npm install @zimic/http @zimic/interceptor --save-dev
pnpm add @zimic/http @zimic/interceptor --dev
yarn add @zimic/http @zimic/interceptor --dev
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
- pnpm
- yarn
npm install @zimic/http@canary @zimic/interceptor@canary --save-dev
pnpm add @zimic/http@canary @zimic/interceptor@canary --dev
yarn add @zimic/http@canary @zimic/interceptor@canary --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.
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.
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.
HTTP interceptors are available in two types: local
(default) and remote
.
- Local interceptor
- Remote interceptor
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
});
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'remote',
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();
});
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.
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.
- Local interceptor
- Remote interceptor
beforeEach(() => {
interceptor.clear();
});
beforeEach(async () => {
await 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.
- Local interceptor
- Remote interceptor
afterEach(() => {
interceptor.checkTimes();
});
afterEach(async () => {
await 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.
- Local interceptor
- Remote interceptor
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...
});
test('example', async () => {
const users: User[] = [{ username: 'me' }];
await 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...
});
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
.
- Local interceptor
- Remote interceptor
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
const handler = await 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 minimalfetch
-like API client.