Zimic
TypeScript-first HTTP request mocking
npm • Docs • Examples • Issues • Roadmap
Zimic is a lightweight, thoroughly tested, TypeScript-first HTTP request mocking library, inspired by Zod’s type inference.
Features
Zimic provides a flexible and type-safe way to mock HTTP requests.
- ⚡ Statically-typed mocks. Declare your HTTP endpoints and get full static type inference and validation when applying mocks.
- 🔗 Network-level intercepts. Internally, Zimic combines MSW and interceptor servers to act on real HTTP requests. This means that no parts of your code are stubbed or skipped. From you application’s point of view, the mocked requests are indistinguishable from the real ones.
- 🔧 Flexibility. You can simulate real application workflows by mocking any number of endpoints. This is specially useful in testing, making sure that the path your application takes is covered.
- 💡 Simplicity. Zimic was designed from scratch to encourage clarity, simplicity and developer experience in your mocks. Check our getting started guide and starting mocking!
[!NOTE]
Zimic has gone a long way in v0, but we’re not yet v1!
Reviews and improvements to the public API are possible, so breaking changes may exceptionally land without a major release during v0. Despite of that, we do not expect big mental model shifts. Usually, migrating to a new Zimic release requires minimal to no refactoring. During v0, we will follow these guidelines:
- Breaking changes, if any, will be delivered in the next minor version.
- Breaking changes, if any, will be documented in the version release, along with a migration guide detailing the introduced changes and suggesting steps to migrate.
From v0.8 onwards, we expect Zimic’s public API to become more stable. If you’d like to share any feedback, please feel free to open an issue or create a discussion!
Table of contents
- Features
- Table of contents
- Getting started
- Examples
- Usage
zimic
API referencezimic/interceptor
API referenceHttpInterceptor
HttpRequestHandler
- Intercepted HTTP resources
- CLI
- Changelog
Getting started
1. Requirements
TypeScript >= 4.7
strict
enabled in yourtsconfig.json
:{ // ... "compilerOptions": { // ... "strict": true, }, }
npm
2. Install from Manager | Command |
---|---|
npm | npm install zimic --save-dev |
pnpm | pnpm add zimic --dev |
yarn | yarn add zimic --dev |
bun | bun add zimic --dev |
The latest (possibly unstable) code is available in canary releases, under the tag canary
:
Manager | Command |
---|---|
npm | npm install zimic@canary --save-dev |
pnpm | pnpm add zimic@canary --dev |
yarn | yarn add zimic@canary --dev |
bun | bun add zimic@canary --dev |
3. Choose your method to intercept requests
Zimic interceptors support two types of execution: local
and remote
.
[!TIP]
The type is an individual interceptor setting. It is perfectly possible to have multiple interceptors with different types in the same application! However, keep in mind that local interceptors have precedence over remote interceptors.
Local HTTP interceptors
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.
When to use local
:
- Testing: If you run your application in the same process as your tests. This is common when using unit and integration test runners such as Jest and Vitest.
- Development: If you want to mock requests in your development environment without setting up a server. This is be useful when you need a backend that is not ready or available.
Our Vitest, Jest, and Next.js Pages Router examples use local interceptors.
[!IMPORTANT]
All mocking operations in local interceptor are synchronous. There’s no need to
await
them before making requests.
Remote HTTP interceptors
When an interceptor is remote
, Zimic uses a dedicated local 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.
When to use remote
:
- Testing: If you do not run your application in the same process as your tests. When using Cypress, Playwright, or other end-to-end testing tools, this is generally the case because the test runner and the application run in separate processes. This might also happen in more complex setups with unit and integration test runners, such as testing a server that is running in another process, terminal, or machine.
- Development: If you want your mocked responses to be accessible by other processes in your local network (e.g.
browser, app,
curl
) . A common scenario is to create a mock server along with a script to apply the mocks. After started, the server can be accessed by other applications and return mock responses.
Our Playwright and Next.js App Router examples use remote interceptors.
[!IMPORTANT]
All mocking operations in remote interceptors are asynchronous. Make sure to
await
them before making requests.Many code snippets in this
README.md
show examples with a local and a remote interceptor. Generally, the remote snippets differ only by addingawait
where necessary.If you are using
typescript-eslint
, a handy rule is@typescript-eslint/no-floating-promises
. It checks that no promises are unhandled, avoiding forgetting toawait
remote interceptor operations.
4. Post-install
Node.js post-install
No additional configuration is necessary for Node.js. Check out the usage guide and start mocking!
Browser post-install
If you plan to use local interceptors and run Zimic in a browser, you must first initialize a mock service worker in your public directory. After that, check out the usage guide and start mocking!
Examples
Visit our examples to see how to use Zimic with popular frameworks and libraries!
Usage
Basic usage
To start using Zimic, create your first HTTP interceptor:
Local
import { JSONValue } from 'zimic'; import { httpInterceptor } from 'zimic/interceptor/http'; type User = JSONValue<{ username: string; }>; const interceptor = httpInterceptor.create<{ '/users': { GET: { response: { 200: { body: User[] }; }; }; }; }>({ type: 'local', baseURL: 'http://localhost:3000', });
Remote
import { JSONValue } from 'zimic'; import { httpInterceptor } from 'zimic/interceptor/http'; type User = JSONValue<{ username: string; }>; const interceptor = httpInterceptor.create<{ '/users': { GET: { response: { 200: { body: User[] }; }; }; }; }>({ type: 'remote', // The interceptor server is at http://localhost:4000 baseURL: 'http://localhost:4000/my-service', });
In this example, we’re creating an interceptor for a service supporting
GET
requests to/users
. A successful response contains an array ofUser
objects. Learn more about declaring HTTP service schemas.Then, start the interceptor:
await interceptor.start();
If you are creating a remote interceptor, it’s necessary to have a running interceptor server before starting it. The base URL of the remote interceptor should point to the server, optionally including a path to differentiate from other interceptors.
Now, you can intercept requests and return mock responses!
Local
const listHandler = interceptor.get('/users').respond({ status: 200, body: [{ username: 'diego-aquino' }], }); const response = await fetch('http://localhost:3000/users'); const users = await response.json(); console.log(users); // [{ username: 'diego-aquino' }]
Remote
const listHandler = await interceptor.get('/users').respond({ status: 200, body: [{ username: 'diego-aquino' }], }); const response = await fetch('http://localhost:3000/users'); const users = await response.json(); console.log(users); // [{ username: 'diego-aquino' }]
More usage examples and recommendations are available in our examples and the
zimic/interceptor
API reference.
Testing
We recommend managing the lifecycle of your interceptors using beforeAll
and afterAll
, or equivalent hooks, in your
test setup file. An example using a Jest/Vitest API:
tests/setup.ts
Local// Your interceptors
const interceptors = [userInterceptor, analyticsInterceptor];
// Start intercepting requests
beforeAll(async () => {
for (const interceptor of interceptors) {
await interceptor.start();
}
});
// Clear all interceptors so that no tests affect each other
afterEach(() => {
for (const interceptor of interceptors) {
interceptor.clear();
}
});
// Stop intercepting requests
afterAll(async () => {
for (const interceptor of interceptors) {
await interceptor.stop();
}
}); |
Remote// Your interceptors
const interceptors = [userInterceptor, analyticsInterceptor];
// Start intercepting requests
beforeAll(async () => {
for (const interceptor of interceptors) {
await interceptor.start();
}
});
// Clear all interceptors so that no tests affect each other
afterEach(async () => {
for (const interceptor of interceptors) {
await interceptor.clear();
}
});
// Stop intercepting requests
afterAll(async () => {
for (const interceptor of interceptors) {
await interceptor.stop();
}
}); |
When using remote interceptors, a common strategy is to apply your mocks before starting the application. See Next.js App Router - Loading mocks and Playwright - Loading mocks for examples.
zimic
API reference
This module provides general resources, such as HTTP classes and types.
[!TIP]
All APIs are documented using JSDoc and visible directly in your IDE.
HttpHeaders
A superset of the built-in Headers
class, with a strictly-typed
schema. HttpHeaders
is fully compatible with Headers
and is used by Zimic to provide type safety when managing
headers.
HttpHeaders
example:
import { HttpHeaders } from 'zimic/http';
const headers = new HttpHeaders<{
accept?: string;
'content-type'?: string;
}>({
accept: '*/*',
'content-type': 'application/json',
});
const contentType = headers.get('content-type');
console.log(contentType); // 'application/json'
HttpHeaders
Comparing HttpHeaders
also provides the utility methods headers.equals()
and headers.contains()
, useful in comparisons with
other headers:
Comparing HttpHeaders
example:
import { HttpSchema, HttpHeaders } from 'zimic/http';
type HeaderSchema = HttpSchema.Headers<{
accept?: string;
'content-type'?: string;
}>;
const headers1 = new HttpHeaders<HeaderSchema>({
accept: '*/*',
'content-type': 'application/json',
});
const headers2 = new HttpHeaders<HeaderSchema>({
accept: '*/*',
'content-type': 'application/json',
});
const headers3 = new HttpHeaders<
HeaderSchema & {
'x-custom-header'?: string;
}
>({
accept: '*/*',
'content-type': 'application/json',
'x-custom-header': 'value',
});
console.log(headers1.equals(headers2)); // true
console.log(headers1.equals(headers3)); // false
console.log(headers1.contains(headers2)); // true
console.log(headers1.contains(headers3)); // false
console.log(headers3.contains(headers1)); // true
HttpSearchParams
A superset of the built-in URLSearchParams
class, with a
strictly-typed schema. HttpSearchParams
is fully compatible with URLSearchParams
and is used by Zimic to provide
type safety when managing search parameters.
HttpSearchParams
example:
import { HttpSearchParams } from 'zimic/http';
const searchParams = new HttpSearchParams<{
names?: string[];
page?: `${number}`;
}>({
names: ['user 1', 'user 2'],
page: '1',
});
const names = searchParams.getAll('names');
console.log(names); // ['user 1', 'user 2']
const page = searchParams.get('page');
console.log(page); // '1'
HttpSearchParams
Comparing HttpSearchParams
also provides the utility methods searchParams.equals()
and searchParams.contains()
, useful in
comparisons with other search params:
Comparing HttpSearchParams
example:
import { HttpSchema, HttpSearchParams } from 'zimic/http';
type SearchParamsSchema = HttpSchema.SearchParams<{
names?: string[];
page?: `${number}`;
}>;
const searchParams1 = new HttpSearchParams<SearchParamsSchema>({
names: ['user 1', 'user 2'],
page: '1',
});
const searchParams2 = new HttpSearchParams<SearchParamsSchema>({
names: ['user 1', 'user 2'],
page: '1',
});
const searchParams3 = new HttpSearchParams<
SearchParamsSchema & {
orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}[]`;
}
>({
names: ['user 1', 'user 2'],
page: '1',
orderBy: ['name.asc'],
});
console.log(searchParams1.equals(searchParams2)); // true
console.log(searchParams1.equals(searchParams3)); // false
console.log(searchParams1.contains(searchParams2)); // true
console.log(searchParams1.contains(searchParams3)); // false
console.log(searchParams3.contains(searchParams1)); // true
HttpFormData
A superset of the built-in FormData
class, with a
strictly-typed schema. HttpFormData
is fully compatible with FormData
and is used by Zimic to provide type safety
when managing form data.
HttpFormData
example:
import { HttpFormData } from 'zimic/http';
const formData = new HttpFormData<{
files: File[];
description?: string;
}>();
formData.append('file', new File(['content'], 'file.txt', { type: 'text/plain' }));
formData.append('description', 'My file');
const files = formData.getAll('file');
console.log(files); // [File { name: 'file.txt', type: 'text/plain' }]
const description = formData.get('description');
console.log(description); // 'My file'
HttpFormData
Comparing HttpFormData
also provides the utility methods formData.equals()
and formData.contains()
, useful in comparisons
with other form data:
Comparing HttpFormData
example:
import { HttpSchema, HttpFormData } from 'zimic/http';
type FormDataSchema = HttpSchema.FormData<{
files: File[];
description?: string;
}>;
const formData1 = new HttpFormData<FormDataSchema>();
formData1.append('file', new File(['content'], 'file.txt', { type: 'text/plain' }));
formData1.append('description', 'My file');
const formData2 = new HttpFormData<FormDataSchema>();
formData2.append('file', new File(['content'], 'file.txt', { type: 'text/plain' }));
formData2.append('description', 'My file');
const formData3 = new HttpFormData<FormDataSchema>();
formData3.append('file', new File(['content'], 'file.txt', { type: 'text/plain' }));
formData3.append('description', 'My file');
console.log(formData1.equals(formData2)); // true
console.log(formData1.equals(formData3)); // false
console.log(formData1.contains(formData2)); // true
console.log(formData1.contains(formData3)); // true
console.log(formData3.contains(formData1)); // false
zimic/interceptor
API reference
This module provides resources to create HTTP interceptors for both Node.js and browser environments.
HttpInterceptor
HTTP interceptors provide the main API to handle HTTP requests and return mock responses. The methods, paths, status codes, parameters, and responses are statically-typed based on the service schema.
Each interceptor represents a service and can be used to mock its paths and methods.
httpInterceptor.create
Creates an HTTP interceptor, the main interface to intercept HTTP requests and return responses. Learn more about declaring service schemas.
Creating a local HTTP interceptor
A local interceptor is configured with type: 'local'
. The baseURL
represents the URL should be matched by this
interceptor. Any request starting with the baseURL
will be intercepted if a matching handler
exists.
import { JSONValue } from 'zimic';
import { httpInterceptor } from 'zimic/interceptor/http';
type User = JSONValue<{
username: string;
}>;
const interceptor = httpInterceptor.create<{
'/users/:id': {
GET: {
response: {
200: { body: User };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Creating a remote HTTP interceptor
A remote interceptor is configured with type: 'remote'
. The baseURL
points to an
interceptor server. Any request starting with the baseURL
will be intercepted if a matching
handler exists.
import { JSONValue } from 'zimic';
import { httpInterceptor } from 'zimic/interceptor/http';
type User = JSONValue<{
username: string;
}>;
const interceptor = httpInterceptor.create<{
'/users/:id': {
GET: {
response: {
200: { body: User };
};
};
};
}>({
// The interceptor server is at http://localhost:4000
// `/my-service` is a path to differentiate from other
// interceptors using the same server
type: 'remote',
baseURL: 'http://localhost:4000/my-service',
});
A single interceptor server is perfectly capable of handling multiple interceptors and requests. Thus, additional paths are supported and might be necessary to differentiate between conflicting interceptors. If you may have multiple threads or processes applying mocks concurrently to the same interceptor server, it’s important to keep the interceptor base URLs unique. Also, make sure that your application is considering the correct URL when making requests.
const interceptor = httpInterceptor.create<{
// ...
}>({
type: 'remote',
// Declaring a base URL with a unique identifier to prevent conflicts
baseURL: `http://localhost:4000/my-service-${crypto.randomUUID()}`,
});
// Your application should use this base URL when making requests
const baseURL = interceptor.baseURL();
Unhandled requests
When a request is not matched by any interceptor handlers, it is considered unhandled and will be logged to the console by default.
[!TIP]
If you expected a request to be handled, but it was not, make sure that the interceptor base URL, path, method, and restrictions correctly match the request. Additionally, confirm that no errors occurred while creating the response.
In a local interceptor, unhandled requests are always bypassed, meaning that they pass through the interceptor and reach the real network. Remote interceptors in pair with an interceptor server always reject unhandled requests because they cannot be bypassed.
You can override the default logging behavior per interceptor with onUnhandledRequest
in httpInterceptor.create()
.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
onUnhandledRequest: { log: false },
});
onUnhandledRequest
also accepts a function to dynamically choose when to ignore an unhandled request. Calling
await context.log()
logs the request to the console. Learn more about the request
object at
Intercepted HTTP resources.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
onUnhandledRequest: async (request, context) => {
const url = new URL(request.url);
// Ignore only unhandled requests to /assets
if (!url.pathname.startsWith('/assets')) {
await context.log();
}
},
});
If you want to override the default logging behavior for all interceptors, or requests that did not match any known base
URL, you can use httpInterceptor.default.onUnhandledRequest
. Keep in mind that defining an onUnhandledRequest
when
creating an interceptor will take precedence over httpInterceptor.default.onUnhandledRequest
.
import { httpInterceptor } from 'zimic/interceptor/http';
// Example 1: Ignore all unhandled requests
httpInterceptor.default.onUnhandledRequest({ log: false });
// Example 2: Ignore only unhandled requests to /assets
httpInterceptor.default.onUnhandledRequest((request, context) => {
const url = new URL(request.url);
if (!url.pathname.startsWith('/assets')) {
await context.log();
}
});
Saving intercepted requests
The option saveRequests
indicates whether request handlers should save their intercepted
requests in memory and make them accessible through handler.requests()
.
This setting is configured per interceptor and is false
by default. If set to true
, each handler will keep track of
their intercepted requests in memory.
[!IMPORTANT]
Saving the intercepted requests will lead to a memory leak if not accompanied by clearing of the interceptor or disposal of the handlers (i.e. garbage collection).
If you plan on accessing those requests, such as to assert them in your tests, set
saveRequests
totrue
and make sure to regularly clear the interceptor. A common practice is to callinterceptor.clear()
after each test.See Testing for an example of how to manage the lifecycle of interceptors in your tests.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
saveRequests: true,
});
[!TIP]
If you use an interceptor both in tests and as a standalone mock server, consider setting
saveRequests
based on an environment variable. This allows you to access the requests in tests, while preventing memory leaks in long-running mock servers.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
saveRequests: process.env.NODE_ENV === 'test',
});
Declaring HTTP service schemas
HTTP service schemas define the structure of the real services being mocked. This includes paths, methods, request and response bodies, and status codes. Based on the schema, interceptors will provide type validation when applying mocks.
An example of a complete interceptor schema:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
// Declaring base types
type User = JSONValue<{
username: string;
}>;
type UserCreationBody = JSONValue<{
username: string;
}>;
type NotFoundError = JSONValue<{
message: string;
}>;
type UserListSearchParams = HttpSchema.SearchParams<{
name?: string;
orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}`[];
}>;
// Creating the interceptor
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: {
headers: { accept: string };
body: UserCreationBody;
};
response: {
201: {
headers: { 'content-type': string };
body: User;
};
};
};
GET: {
request: {
searchParams: UserListSearchParams;
};
response: {
200: { body: User[] };
404: { body: NotFoundError };
};
};
};
'/users/:id': {
GET: {
response: {
200: { body: User };
404: { body: NotFoundError };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Alternatively, you can compose the schema using utility types:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
// Declaring the base types
type User = JSONValue<{
username: string;
}>;
type UserCreationBody = JSONValue<{
username: string;
}>;
type NotFoundError = JSONValue<{
message: string;
}>;
type UserListSearchParams = HttpSchema.SearchParams<{
name?: string;
orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}`[];
}>;
// Declaring user methods
type UserMethods = HttpSchema.Methods<{
POST: {
request: {
headers: { accept: string };
body: UserCreationBody;
};
response: {
201: {
headers: { 'content-type': string };
body: User;
};
};
};
GET: {
request: {
searchParams: UserListSearchParams;
};
response: {
200: { body: User[] };
404: { body: NotFoundError };
};
};
}>;
type UserByIdMethods = HttpSchema.Methods<{
GET: {
response: {
200: { body: User };
404: { body: NotFoundError };
};
};
}>;
// Declaring user paths
type UserPaths = HttpSchema.Paths<{
'/users': UserMethods;
}>;
type UserByIdPaths = HttpSchema.Paths<{
'/users/:id': UserByIdMethods;
}>;
// Declaring interceptor schema
type ServiceSchema = UserPaths & UserByIdPaths;
// Creating the interceptor
const interceptor = httpInterceptor.create<ServiceSchema>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring HTTP paths
At the root level, each key represents a path or route of the service:
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
// Path schema
};
'/users/:id': {
// Path schema
};
'/posts': {
// Path schema
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Alternatively, you can also compose paths using the utility type HttpSchema.Paths
:
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserPaths = HttpSchema.Paths<{
'/users': {
// Path schema
};
'/users/:id': {
// Path schema
};
}>;
type PostPaths = HttpSchema.Paths<{
'/posts': {
// Path schema
};
}>;
const interceptor = httpInterceptor.create<UserPaths & PostPaths>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring HTTP methods
Each path can have one or more methods, (GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, and OPTIONS
). The method
names are case-sensitive.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
GET: {
// Method schema
};
POST: {
// Method schema
};
};
// Other paths
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Similarly to paths, you can also compose methods using the utility type
HttpSchema.Methods
:
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserMethods = HttpSchema.Methods<{
GET: {
// Method schema
};
POST: {
// Method schema
};
}>;
const interceptor = httpInterceptor.create<{
'/users': UserMethods;
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring HTTP requests
Each method can have a request
, which defines the schema of the accepted requests. headers
, searchParams
, and
body
are supported to provide type safety when applying mocks. Path parameters are
automatically inferred from dynamic paths, such as /users/:id
.
Declaring a request type with search params:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserListSearchParams = HttpSchema.SearchParams<{
username?: string;
}>;
const interceptor = httpInterceptor.create<{
'/users': {
GET: {
request: { searchParams: UserListSearchParams };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a request type with JSON body:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserCreationBody = JSONValue<{
username: string;
}>;
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: { body: UserCreationBody };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
[!IMPORTANT]
JSON body types cannot be declared using TypeScript interfaces, because they do not have implicit index signatures as types do. Part of Zimic’s JSON validation relies on index signatures. To workaround this, you can declare JSON bodies using
type
. As an extra step to make sure the type is a valid JSON, you can use the utility typeJSONValue
.
[!TIP]
The utility type
JSONSerialized
, exported fromzimic
, can be handy to infer the serialized type of an object. It convertsDate
’s to strings, removes function properties and serializes nested objects and arrays.
import { JSONSerialized } from 'zimic/http';
class User {
name: string;
age: number;
createdAt: Date;
method() {
// ...
}
}
type SerializedUser = JSONSerialized<User>;
// { name: string, age: number, createdAt: string }
Declaring a request type with form data body:
import { HttpSchema, HttpFormData } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type FileUploadData = HttpSchema.FormData<{
files: File[];
description?: string;
}>;
const interceptor = httpInterceptor.create<{
'/files': {
POST: {
request: { body: HttpFormData<FileUploadData> };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a request type with blob body:
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: { body: Blob };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a request type with plain text body:
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: { body: string };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a request type with search params (x-www-form-urlencoded
) body:
import { HttpSchema, HttpSearchParams } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserListSearchParams = HttpSchema.SearchParams<{
username?: string;
}>;
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: { body: HttpSearchParams<UserListSearchParams> };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
[!TIP]
You only need to include in the schema the properties you want to use in your mocks. Headers, search params, or body fields that are not used do not need to be declared, keeping your type definitions clean and concise.
You can also compose requests using the utility type HttpSchema.Request
, similarly to
methods:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserCreationBody = JSONValue<{
username: string;
}>;
type UserCreationRequest = HttpSchema.Request<{
body: UserCreationBody;
}>;
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
request: UserCreationRequest;
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring HTTP responses
Each method can also have a response
, which defines the schema of the returned responses. The status codes are used as
keys. headers
and body
are supported to provide type safety when applying mocks.
Bodies can be a JSON object, HttpFormData
, HttpSearchParams
, Blob
, or plain
text.
Declaring a response type with JSON body:
import { JSONValue } from 'zimic';
import { httpInterceptor } from 'zimic/interceptor/http';
type User = JSONValue<{
username: string;
}>;
type NotFoundError = JSONValue<{
message: string;
}>;
const interceptor = httpInterceptor.create<{
'/users/:id': {
GET: {
response: {
200: { body: User };
404: { body: NotFoundError };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
[!IMPORTANT]
Also similarly to declaring HTTP requests, JSON body types cannot be declared using TypeScript interfaces, because they do not have implicit index signatures as types do. Part of Zimic’s JSON validation relies on index signatures. To workaround this, you can declare bodies using
type
. As an extra step to make sure the type is a valid JSON, you can use the utility typeJSONValue
.
Declaring a response type with form data body:
import { HttpSchema, HttpFormData } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type FileUploadData = HttpSchema.FormData<{
files: File[];
description?: string;
}>;
const interceptor = httpInterceptor.create<{
'/files': {
POST: {
response: {
200: { body: HttpFormData<FileUploadData> };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a response type with blob body:
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
response: {
200: { body: Blob };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a response type with plain text body:
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
response: {
200: { body: string };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Declaring a response type with search params (x-www-form-urlencoded
) body:
import { HttpSchema, HttpSearchParams } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type UserListSearchParams = HttpSchema.SearchParams<{
username?: string;
}>;
const interceptor = httpInterceptor.create<{
'/users': {
POST: {
response: {
200: { body: HttpSearchParams<UserListSearchParams> };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
[!TIP]
Similarly to declaring HTTP requests, you only need to include in the schema the properties you want to use in your mocks. Headers, search params, or body fields that are not used do not need to be declared, keeping your type definitions clean and concise.
You can also compose responses using the utility types HttpSchema.ResponseByStatusCode
and
HttpSchema.Response
, similarly to requests:
import { JSONValue } from 'zimic';
import { HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
type User = JSONValue<{
username: string;
}>;
type NotFoundError = JSONValue<{
message: string;
}>;
type SuccessUserGetResponse = HttpSchema.Response<{
body: User;
}>;
type NotFoundUserGetResponse = HttpSchema.Response<{
body: NotFoundError;
}>;
type UserGetResponses = HttpSchema.ResponseByStatusCode<{
200: SuccessUserGetResponse;
404: NotFoundUserGetResponse;
}>;
const interceptor = httpInterceptor.create<{
'/users/:id': {
GET: {
response: UserGetResponses;
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
interceptor.start()
HTTP Starts the interceptor. Only interceptors that are running will intercept requests.
await interceptor.start();
When targeting a browser environment with a local interceptor, make sure to follow the browser post-install guide before starting your interceptors.
interceptor.stop()
HTTP Stops the interceptor. Stopping an interceptor will also clear its registered handlers and responses.
await interceptor.stop();
interceptor.isRunning()
HTTP Returns whether the interceptor is currently running and ready to use.
const isRunning = interceptor.isRunning();
interceptor.baseURL()
HTTP Returns the base URL of the interceptor.
const baseURL = interceptor.baseURL();
interceptor.platform()
HTTP Returns the platform used by the interceptor (browser
or node
).
const platform = interceptor.platform();
interceptor.<method>(path)
HTTP Creates an HttpRequestHandler
for the given method and path. The path and method must be
declared in the interceptor schema.
The supported methods are: get
, post
, put
, patch
, delete
, head
, and options
.
When using a remote interceptor, creating a handler is an asynchronous operation, so you
need to await
it. You can also chain any number of operations and apply them by awaiting the handler.
Localimport { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
GET: {
response: {
200: { body: User[] };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
const listHandler = interceptor.get('/users').respond({
status: 200
body: [{ username: 'diego-aquino' }],
}); |
Remoteimport { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users': {
GET: {
response: {
200: { body: User[] };
};
};
};
}>({
type: 'remote',
baseURL: 'http://localhost:4000/my-service',
});
const listHandler = await interceptor.get('/users').respond({
status: 200
body: [{ username: 'diego-aquino' }],
}); |
Dynamic path parameters
Paths with dynamic path parameters are supported, such as /users/:id
. Even when using a computed path (e.g.
/users/1
), the original path is automatically inferred, guaranteeing type safety.
import { httpInterceptor } from 'zimic/interceptor/http';
const interceptor = httpInterceptor.create<{
'/users/:id': {
PUT: {
request: {
body: { username: string };
};
response: {
204: {};
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
interceptor.get('/users/:id'); // Matches any id
interceptor.get(`/users/${1}`); // Only matches id 1
request.pathParams
contains the parsed path parameters of a request and have their type automatically inferred from
the path string. For example, the path /users/:userId
will result in a request.pathParams
of type
{ userId: string }
.
Localconst updateHandler = interceptor.put('/users/:id').respond((request) => {
console.log(request.pathParams); // { id: '1' }
return {
status: 200,
body: { username: 'diego-aquino' },
};
});
await fetch('http://localhost:3000/users/1', { method: 'PUT' }); |
Remoteconst updateHandler = await interceptor.put('/users/:id').respond((request) => {
console.log(request.pathParams); // { id: '1' }
return {
status: 200,
body: { username: 'diego-aquino' },
};
});
await fetch('http://localhost:3000/users/1', { method: 'PUT' }); |
interceptor.clear()
HTTP Clears all of the HttpRequestHandler
instances created by this interceptor, including their
registered responses and intercepted requests. After calling this method, the interceptor will no longer intercept any
requests until new mock responses are registered.
This method is useful to reset the interceptor mocks between tests.
Localinterceptor.clear(); |
Remoteawait interceptor.clear(); |
HttpRequestHandler
HTTP request handlers allow declaring HTTP responses to return for intercepted requests. They also keep track of the intercepted requests and their responses, which can be used to check if the requests your application has made are correct.
When multiple handlers match the same method and path, the last created with
interceptor.<method>(path)
will be used.
handler.method()
HTTP Returns the method that matches a handler.
Localconst handler = interceptor.post('/users');
const method = handler.method();
console.log(method); // 'POST' |
Remoteconst handler = await interceptor.post('/users');
const method = handler.method();
console.log(method); // 'POST' |
handler.path()
HTTP Returns the path that matches a handler. The base URL of the interceptor is not included, but it is used when matching requests.
Localconst handler = interceptor.get('/users');
const path = handler.path();
console.log(path); // '/users' |
Remoteconst handler = await interceptor.get('/users');
const path = handler.path();
console.log(path); // '/users' |
handler.with(restriction)
HTTP Declares a restriction to intercepted requests. headers
, searchParams
, and body
are supported to limit which
requests will match the handler and receive the mock response. If multiple restrictions are declared, either in a single
object or with multiple calls to handler.with()
, all of them must be met, essentially creating an AND condition.
Static restrictions
Declaring restrictions for headers:
Localconst creationHandler = interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst creationHandler = await interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
An equivalent alternative using HttpHeaders
:
Localconst headers = new HttpHeaders<Partial<UserListHeaders>>();
headers.set('authorization', `Bearer ${token}`);
const creationHandler = interceptor
.get('/users')
.with({ headers })
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst headers = new HttpHeaders<Partial<UserListHeaders>>();
headers.set('authorization', `Bearer ${token}`);
const creationHandler = await interceptor
.get('/users')
.with({ headers })
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Declaring restrictions for search params:
Localconst creationHandler = interceptor
.get('/users')
.with({
searchParams: { username: 'diego-aquino' },
})
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst creationHandler = await interceptor
.get('/users')
.with({
searchParams: { username: 'diego-aquino' },
})
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
An equivalent alternative using HttpSearchParams
:
Localconst searchParams = new HttpSearchParams<Partial<UserListSearchParams>>();
searchParams.set('username', 'diego-aquino');
const creationHandler = interceptor
.get('/users')
.with({ searchParams })
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst searchParams = new HttpSearchParams<Partial<UserListSearchParams>>();
searchParams.set('username', 'diego-aquino');
const creationHandler = await interceptor
.get('/users')
.with({ searchParams })
.respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Declaring restrictions for a JSON body:
Localconst creationHandler = interceptor
.post('/users')
.with({
body: { username: 'diego-aquino' },
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Remoteconst creationHandler = await interceptor
.post('/users')
.with({
body: { username: 'diego-aquino' },
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
For JSON bodies to be correctly parsed, make sure that the intercepted requests have the header
content-type: application/json
.
Declaring restrictions for a form data body:
Localimport { HttpFormData } from 'zimic/http';
const formData = new HttpFormData<Partial<UserCreationData>>();
formData.append('username', 'diego-aquino');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const creationHandler = interceptor
.post('/users')
.with({
body: formData,
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Remoteimport { HttpFormData } from 'zimic/http';
const formData = new HttpFormData<Partial<UserCreationData>>();
formData.append('username', 'diego-aquino');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const creationHandler = await interceptor
.post('/users')
.with({
body: formData,
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
For form data bodies to be correctly parsed, make sure that the intercepted requests have the header
content-type: multipart/form-data
.
Declaring restrictions for a blob body:
Localconst creationHandler = interceptor
.post('/users')
.with({
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Remoteconst creationHandler = await interceptor
.post('/users')
.with({
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
For blob bodies to be correctly parsed, make sure that the intercepted requests have the header content-type
indicating a binary data, such as application/octet-stream
, image/png
, audio/mp3
, etc.
Declaring restrictions for a plain text body:
Localconst creationHandler = interceptor
.post('/users')
.with({
body: 'content',
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Remoteconst creationHandler = await interceptor
.post('/users')
.with({
body: 'content',
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
For plain text bodies to be correctly parsed, make sure that the intercepted requests have the header content-type
indicating a plain text, such as text/plain
.
By default, restrictions use exact: false
, meaning that any request containing the declared restrictions will
match the handler, regardless of having more properties or values. In the examples above, requests with more properties
in the headers, search params, or body would still match the restrictions.
If you want to match only requests with the exact values declared, you can use exact: true
:
Localconst creationHandler = interceptor
.post('/users')
.with({
headers: { 'content-type': 'application/json' },
body: { username: 'diego-aquino' },
exact: true, // Only requests with these exact headers and body will match
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Remoteconst creationHandler = await interceptor
.post('/users')
.with({
headers: { 'content-type': 'application/json' },
body: { username: 'diego-aquino' },
exact: true, // Only requests with these exact headers and body will match
})
.respond({
status: 201,
body: { username: 'diego-aquino' },
}); |
Computed restrictions
A function is also supported to declare restrictions in case they are dynamic. Learn more about the request
object at
Intercepted HTTP resources.
Localconst creationHandler = interceptor
.post('/users')
.with((request) => {
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst creationHandler = await interceptor
.post('/users')
.with((request) => {
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: [{ username: 'diego-aquino' }],
}); |
The function should return a boolean: true
if the request matches the handler and should receive the mock response;
false
otherwise.
handler.respond(declaration)
HTTP Declares a response to return for matched intercepted requests.
When the handler matches a request, it will respond with the given declaration. The response type is statically validated against the schema of the interceptor.
Static responses
Declaring responses with JSON body:
Localconst listHandler = interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Remoteconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
}); |
Declaring responses with form data body:
Localimport { HttpFormData } from 'zimic/http';
const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'diego-aquino');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const listHandler = interceptor.get('/users/:id').respond({
status: 200,
body: formData,
}); |
Remoteimport { HttpFormData } from 'zimic/http';
const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'diego-aquino');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const listHandler = await interceptor.get('/users/:id').respond({
status: 200,
body: formData,
}); |
Declaring responses with blob body:
Localconst listHandler = interceptor.get('/users').respond({
status: 200,
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
}); |
Remoteconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
}); |
Declaring responses with plain text body:
Localconst listHandler = interceptor.get('/users').respond({
status: 200,
body: 'content',
}); |
Remoteconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: 'content',
}); |
Declaring responses with search params (x-www-form-urlencoded
) body:
Localimport { HttpSearchParams } from 'zimic/http';
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
username: 'diego-aquino',
});
const listHandler = interceptor.get('/users').respond({
status: 200,
body: searchParams,
}); |
Remoteimport { HttpSearchParams } from 'zimic/http';
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
username: 'diego-aquino',
});
const listHandler = await interceptor.get('/users').respond({
status: 200,
body: searchParams,
}); |
Computed responses
A function is also supported to declare a response in case it is dynamic. Learn more about the request
object at
Intercepted HTTP resources.
Localconst listHandler = interceptor.get('/users').respond((request) => {
const username = request.searchParams.get('username');
if (!username) {
return { status: 400 };
}
return {
status: 200,
body: [{ username }],
};
}); |
Remoteconst listHandler = await interceptor.get('/users').respond((request) => {
const username = request.searchParams.get('username');
if (!username) {
return { status: 400 };
}
return {
status: 200,
body: [{ username }],
};
}); |
handler.bypass()
HTTP Clears any response declared with handler.respond(declaration)
, making the handler
stop matching requests. The next handler, created before this one, that matches the same method and path will be used if
present. If not, the requests of the method and path will not be intercepted.
To make the handler match requests again, register a new response with
handler.respond(declaration)
.
This method is useful to skip a handler. It is more gentle than handler.clear()
, as it only
removed the response, keeping restrictions and intercepted requests.
Localconst listHandler = interceptor.get('/users').respond({
status: 200,
body: [],
});
const otherListHandler = interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
});
otherListHandler.bypass();
// Now, requests GET /users will match `listHandler` and receive an empty array |
Remoteconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: [],
});
const otherListHandler = await interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
});
await otherListHandler.bypass();
// Now, requests GET /users will match `listHandler` and receive an empty array |
handler.clear()
HTTP Clears any response declared with handler.respond(declaration)
, restrictions
declared with handler.with(restriction)
, and intercepted requests, making the handler
stop matching requests. The next handler, created before this one, that matches the same method and path will be used if
present. If not, the requests of the method and path will not be intercepted.
To make the handler match requests again, register a new response with handler.respond()
.
This method is useful to reset handlers to a clean state between tests. It is more aggressive than
handler.bypass()
, as it also clears restrictions and intercepted requests.
Localconst listHandler = interceptor.get('/users').respond({
status: 200,
body: [],
});
const otherListHandler = interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
});
otherListHandler.clear();
// Now, requests GET /users will match `listHandler` and receive an empty array
otherListHandler.requests(); // Now empty |
Remoteconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: [],
});
const otherListHandler = await interceptor.get('/users').respond({
status: 200,
body: [{ username: 'diego-aquino' }],
});
await otherListHandler.clear();
// Now, requests GET /users will match `listHandler` and receive an empty array
await otherListHandler.requests(); // Now empty |
handler.requests()
HTTP Returns the intercepted requests that matched this handler, along with the responses returned to each of them. This is
useful for testing that the correct requests were made by your application. Learn more about the request
and
response
objects at Intercepted HTTP resources.
[!IMPORTANT]
This method can only be used if
saveRequests
was set totrue
when creating the interceptor. See Saving intercepted requests for more information.
Localconst updateHandler = interceptor.put('/users/:id').respond((request) => {
const newUsername = request.body.username;
return {
status: 200,
body: [{ username: newUsername }],
};
});
await fetch(`http://localhost:3000/users/${1}`, {
method: 'PUT',
body: JSON.stringify({ username: 'new' }),
});
const updateRequests = await updateHandler.requests();
expect(updateRequests).toHaveLength(1);
expect(updateRequests[0].pathParams).toEqual({ id: '1' });
expect(updateRequests[0].body).toEqual({ username: 'new' });
expect(updateRequests[0].response.status).toBe(200);
expect(updateRequests[0].response.body).toEqual([{ username: 'new' }]); |
Remoteconst updateHandler = await interceptor.put('/users/:id').respond((request) => {
const newUsername = request.body.username;
return {
status: 200,
body: [{ username: newUsername }],
};
});
await fetch(`http://localhost:3000/users/${1}`, {
method: 'PUT',
body: JSON.stringify({ username: 'new' }),
});
const updateRequests = await updateHandler.requests();
expect(updateRequests).toHaveLength(1);
expect(updateRequests[0].pathParams).toEqual({ id: '1' });
expect(updateRequests[0].body).toEqual({ username: 'new' });
expect(updateRequests[0].response.status).toBe(200);
expect(updateRequests[0].response.body).toEqual([{ username: 'new' }]); |
Intercepted HTTP resources
The intercepted requests and responses are typed based on their interceptor schema.
They are available as simplified objects based on the
Request
and
Response
web APIs. body
contains the parsed body, while
typed headers, path params and search params are in headers
, pathParams
, and searchParams
, respectively.
The body is automatically parsed based on the header content-type
of the request or response. The following table
shows how each type is parsed, where *
indicates any other resource that does not match the previous types:
content-type |
Parsed to |
---|---|
application/json |
JSON |
application/xml |
String |
application/x-www-form-urlencoded |
HttpSearchParams |
application/* (others) |
Blob |
multipart/form-data |
HttpFormData |
multipart/* (others) |
Blob |
text/* |
String |
image/* |
Blob |
audio/* |
Blob |
font/* |
Blob |
video/* |
Blob |
*/* (others) |
JSON if possible, otherwise String |
If no content-type
exists or it is unknown, Zimic tries to parse the body as JSON and falls back to plain text if it
fails.
If you need access to the original Request
and Response
objects, you can use the request.raw
property:
console.log(request.raw); // Request{}
console.log(request.response.raw); // Response{}
CLI
zimic
zimic [command]
Commands:
zimic browser Browser
zimic server Interceptor server
zimic typegen Type generation
Options:
--help Show help [boolean]
--version Show version number [boolean]
[!TIP]
All boolean options in Zimic’s CLI can be prefixed with
--no-
to negate them.For example, all of the options below are equivalent and indicate that comments are disabled:
--no-comments --comments false --comments=falseOn the other hand, all of the options below are also equivalent and indicate that comments are enabled:
--comments --comments true --comments=true
zimic browser
zimic browser init
Initialize the browser service worker configuration.
zimic browser init <publicDirectory>
Positionals:
publicDirectory The path to the public directory of your application.
[string] [required]
This command is necessary to use Zimic in a browser environment. It creates a mockServiceWorker.js
file in the
provided public directory, which is used to intercept requests and mock responses.
If you are using Zimic mainly in tests, we recommend adding the mockServiceWorker.js
to your .gitignore
and adding
this command to a postinstall
scripts in your package.json
. This ensures that the latest service worker script is
being used after upgrading Zimic.
zimic server
An interceptor server is a standalone server that can be used to handle requests and return mock responses. It is used in combination with remote interceptors, which declare which responses the server should return for a given request. Interceptor servers and remote interceptors communicate with remote-procedure calls (RPC) over WebSockets.
zimic server start
Start an interceptor server.
zimic server start [-- onReady]
Positionals:
onReady A command to run when the server is ready to accept connections.
[string]
Options:
-h, --hostname The hostname to start the server on.
[string] [default: "localhost"]
-p, --port The port to start the server on. [number]
-e, --ephemeral Whether the server should stop automatically
after the on-ready command finishes. If no
on-ready command is provided and ephemeral is
true, the server will stop immediately after
starting. [boolean] [default: false]
-l, --log-unhandled-requests Whether to log a warning when no interceptors
were found for the base URL of a request. If an
interceptor was matched, the logging behavior
for that base URL is configured in the
interceptor itself. [boolean]
You can use this command to start an independent server:
zimic server start --port 4000
Or as a prefix of another command:
zimic server start --port 4000 --ephemeral -- npm run test
The command after --
will be executed when the server is ready. The flag --ephemeral
indicates that the server
should automatically stop after the command finishes.
zimic server
programmatic usage
The module zimic/server
exports resources for managing interceptor servers programmatically. Even though we recommend
using the CLI, this module is a valid alternative for more advanced use cases.
An example using the programmatic API and execa
to run a command when the
server is ready:
import { execa as $ } from 'execa';
import { interceptorServer } from 'zimic/interceptor/server';
const server = interceptorServer.create({ hostname: 'localhost', port: 3000 });
await server.start();
// Run a command when the server is ready, assuming the following format:
// node <script> -- <command> [...commandArguments]
const [command, ...commandArguments] = process.argv.slice(3);
await $(command, commandArguments, { stdio: 'inherit' });
await server.stop();
process.exit(0);
The helper function runCommand
is useful to run a shell command in server scripts. The
Next.js App Router and the Playwright examples use
this function to run the application after the interceptor server is ready and all mocks are set up.
zimic typegen
zimic typegen openapi
Generate types from an OpenAPI schema.
zimic typegen openapi <input>
Positionals:
input The path to a local OpenAPI schema file or an URL to fetch it. Version
3.x is supported as YAML or JSON. [string] [required]
Options:
-o, --output The path to write the generated types to. If not provided,
the types will be written to stdout. [string]
-s, --service-name The name of the service to use in the generated types.
[string] [required]
-c, --comments Whether to include comments in the generated types.
[boolean] [default: true]
-p, --prune Whether to remove unused operations and components from
the generated types. This is useful for reducing the size
of the output file. [boolean] [default: true]
-f, --filter One or more expressions to filter the types to generate.
Filters must follow the format `<method> <path>`, where
`<method>` is an HTTP method or `*`, and `<path>` is a
literal path or a glob. Filters are case-sensitive
regarding paths. For example, `GET /users`, `* /users`,
`GET /users/*`, and `GET /users/**/*` are valid filters.
Negative filters can be created by prefixing the
expression with `!`. For example, `!GET /users` will
exclude paths matching `GET /users`. If more than one
positive filter is provided, they will be combined with
OR, while negative filters will be combined with AND.
[array]
-F, --filter-file A path to a file containing filter expressions. One
expression is expected per line and the format is the same
as used in a `--filter` option. Comments are prefixed with
`#`. A filter file can be used alongside additional
`--filter` expressions. [string]
You can use this command to generate types from a local OpenAPI file:
zimic typegen openapi ./schema.yaml \
--output ./schema.ts \
--service-name MyService
Or an URL to fetch it:
zimic typegen openapi https://example.com/api/openapi.yaml \
--output ./schema.ts \
--service-name MyService
Then, you can use the types in your interceptors:
import { httpInterceptor } from 'zimic/interceptor/http';
import { MyServiceSchema } from './schema';
const interceptor = httpInterceptor.create<MyServiceSchema>({
type: 'local',
baseURL: 'http://localhost:3000',
});
Our typegen example demonstrates how to use zimic typegen openapi
to generate types and use
them in your application and interceptors.
zimic typegen openapi
comments
By default, descriptions in the OpenAPI schema are included as comments in the generated types. You can omit them using
--no-comments
or --comments false
.
zimic typegen openapi ./schema.yaml \
--output ./schema.ts \
--service-name MyService \
--no-comments
zimic typegen openapi
pruning
By default, pruning is enabled, meaning that unused types are not generated. If you want all types declared in the
schema to be generated, you can use --no-prune
or --prune false
.
zimic typegen openapi ./schema.yaml \
--output ./schema.ts \
--service-name MyService \
--no-prune
zimic typegen openapi
filtering
You can also filter a subset of paths to generate types for. Combined with pruning, this is useful to reduce the size of the output file and only generate the types you need.
zimic typegen openapi ./schema.yaml \
--output ./schema.ts \
--service-name MyService \
--filter 'GET /users**'
When many filters are used, a filter file can be provided, where each line represents a filter expression and comments
are marked with #
:
filters.txt
# Include any endpoint starting with /users and having any HTTP method
* /users**
# Include any sub-endpoints of /posts with method GET.
GET /posts/**/*
# Include the endpoints /workspaces with methods GET, POST, or PUT.
GET,POST,PUT /workspaces
# Exclude endpoints to get user notifications
!GET /users/*/notifications/**/*
Then, you can use the filter file in the command:
zimic typegen openapi ./schema.yaml \
--output ./schema.ts \
--service-name MyService \
--filter-file ./filters.txt
zimic typegen
programmatic usage
The module zimic/typegen
exports resources for generating types programmatically. We recommend using the CLI, but this
module is a valid alternative for more advanced use cases.
An example using the programmatic API to generate types from an OpenAPI schema:
import { typegen } from 'zimic/typegen';
await typegen.generateFromOpenAPI({
input: './schema.yaml',
output: './schema.ts',
serviceName: 'MyService',
filters: ['* /users**'],
includeComments: true,
prune: true,
});
The parameters of typegen.generateFromOpenAPI
are the same as the CLI options for the zimic typegen openapi
command.
Changelog
The changelog is available on our GitHub Releases page.