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 and using MSW under the hood.
Zimic provides a flexible and type-safe way to mock HTTP requests.
- ⚡ Statically-typed mocks: Declare the
schema of your HTTP endpoints and create
fully typed mocks. If you have an OpenAPI v3 schema, use
zimic typegen
to automatically generate types and keep your mocks in sync with your API. - 🔗 Network-level intercepts: Internally, Zimic combines MSW and interceptor servers to act on real HTTP requests. From you application's point of view, the mocked responses are indistinguishable from the real ones.
- 🔧 Flexibility: Mock external services and reliably test how your application behaves. Simulate success, loading, and error states with ease using standard web APIs.
- 💡 Simplicity: Zimic was designed to encourage clarity, simplicity, and robustness in your mocks. Check our getting started guide and starting mocking!
import { type HttpSchema } from 'zimic/http';
import { httpInterceptor } from 'zimic/interceptor/http';
// 1. Declare your types:
interface User {
username: string;
}
interface RequestError {
code: string;
message: string;
}
// 2. Declare your HTTP schema:
// https://bit.ly/zimic-interceptor-http-schemas
type MySchema = HttpSchema<{
'/users': {
POST: {
request: { body: User };
response: {
201: { body: User };
400: { body: RequestError };
409: { body: RequestError };
};
};
GET: {
request: {
headers: { authorization: string };
searchParams: {
username?: 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 };
};
};
};
}>;
// 3. Create your interceptor:
// https://bit.ly/zimic-interceptor-http#httpinterceptorcreateoptions
const myInterceptor = httpInterceptor.create<MySchema>({
type: 'local',
baseURL: 'http://localhost:3000',
saveRequests: true, // Allow access to `handler.requests()`
});
// 4. Manage your interceptor lifecycle:
// https://bit.ly/zimic-guides-testing
beforeAll(async () => {
// 4.1. Start intercepting requests:
// https://bit.ly/zimic-interceptor-http#http-interceptorstart
await myInterceptor.start();
});
beforeEach(() => {
// 4.2. Clear interceptors so that no tests affect each other:
// https://bit.ly/zimic-interceptor-http#http-interceptorclear
myInterceptor.clear();
});
afterEach(() => {
// 4.3. Check that all expected requests were made:
// https://bit.ly/zimic-interceptor-http#http-interceptorchecktimes
myInterceptor.checkTimes();
});
afterAll(async () => {
// 4.4. Stop intercepting requests:
// https://bit.ly/zimic-interceptor-http#http-interceptorstop
await myInterceptor.stop();
});
// Enjoy mocking!
test('example', async () => {
const users: User[] = [{ username: 'my-user' }];
// 5. Declare your mocks:
// https://bit.ly/zimic-interceptor-http#http-interceptormethodpath
const myHandler = myInterceptor
.get('/users')
// 5.1. Use restrictions to make declarative assertions and narrow down your mocks:
// https://bit.ly/zimic-interceptor-http#http-handlerwithrestriction
.with({
headers: { authorization: 'Bearer my-token' },
searchParams: { username: 'my' },
})
// 5.2. Respond with your mock data:
// https://bit.ly/zimic-interceptor-http#http-handlerresponddeclaration
.respond({
status: 200,
body: users,
})
// 5.3. Define how many requests you expect your application to make:
// https://bit.ly/zimic-interceptor-http#http-handlertimes
.times(1);
// 6. Run your application and make requests:
// ...
// 7. Check the requests you expect:
// https://bit.ly/zimic-interceptor-http#http-handlerrequests
//
// NOTE: The code below checks the requests manually. This is optional in this
// example because the `with` and `times` calls act as a declarative validation,
// meaning that exactly one request is expected with specific data. If fewer or
// more requests are received, the test will fail when `myInterceptor.checkTimes()`
// is called in the `afterEach` hook.
const requests = myHandler.requests();
expect(requests).toHaveLength(1);
expect(requests[0].headers.get('authorization')).toBe('Bearer my-token');
expect(requests[0].searchParams.size).toBe(1);
expect(requests[0].searchParams.get('username')).toBe('my');
expect(requests[0].body).toBe(null);
});
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!
Visit our examples to see how to use Zimic with popular frameworks, libraries, and use cases!
The changelog is available on our GitHub Releases page.
Interested in contributing to Zimic? Check out our contributing guide to get started!