zomoc
v0.2.0
Published
A type-safe API mocking tool for frontend development, powered by axios and Zod.
Maintainers
Readme
Zomoc: A Type-Safe API Mocking Tool
Zomoc is a tool that decouples your frontend development from unstable backend APIs by automatically generating realistic mock data based on TypeScript interfaces. It can be applied to non-Vite environments like Next.js and Create React App via its CLI, and seamlessly integrates as a plugin in a Vite environment to provide a convenient developer experience.
Zomoc is not a zero-config tool, but a "low-config" one. With just a few lines of configuration, you can create a powerful, type-safe development environment that allows you to work independently of the backend's status.
Features
- Reliable API Decoupling: Keep developing your UI without interruption, even when the real API is down or changing.
- Structural Type-Safety: Guarantees your mock data's structure always matches your TypeScript interfaces, preventing data-shape-related bugs.
- Highly Automated: Scans your code to generate mock data, keeping it in sync with your type definitions.
- Flexible File Structure: Locate your mock definitions and interface files anywhere in your project using glob patterns.
- Dynamic Route Matching: Supports dynamic URL patterns like
/users/:idout of the box. - Pagination Support: Automatically generates paginated responses and intelligently handles both paginated and regular array types.
- Custom Data Generators: Hook in your own data generation logic (e.g., using
@faker-js/faker) for more realistic mock data.
Installation
npm install -D zomoczomoc has axios and zod as peer dependencies. npm 7+ will install them automatically if they are not already in your project.
🚀 Getting Started (CLI)
For environments that don't use Vite (like Next.js or Create React App), you can use zomoc via its CLI. This approach works by generating a physical configuration file instead of using a virtual module.
Step 1: Define Mocks and Types
This step is identical to the Vite version. Create a mock.json file and a corresponding interface.ts file.
src/api/mock.json
{
"GET /users": "IUserListResponse"
}src/api/interface.ts
export interface IUser {
id: string
name: string
email: string
}
export interface IUserListResponse {
users: IUser[]
total: number
}Step 2: Generate the Registry File
Run the generate command in your terminal to create the mock configuration file.
npx zomoc generateThis command creates a .zomoc/registry.ts file in your project root. You'll need to re-run this command whenever you change your mock.json or type files.
Pro-Tip: To greatly improve your development experience, set up a script in your
package.jsonto runnpx zomoc generate --watchalongside yourdevcommand. This will automatically regenerate the file on changes.
Step 3: Activate the Interceptor
Set up the interceptor by importing directly from the file generated by the CLI.
// src/shared/api/index.ts
import axios from "axios"
// Use 'zomoc/cli' instead of 'zomoc'.
import { setupMockingInterceptor } from "zomoc/cli"
// Import directly from the generated file.
import { finalSchemaUrlMap } from "../../.zomoc/registry"
const api = axios.create({ baseURL: "https://api.example.com" })
setupMockingInterceptor(api, {
enabled: process.env.NODE_ENV === "development",
registry: finalSchemaUrlMap,
debug: true, // Optional: log mocked requests
})
export { api }🚀 Getting Started (Vite Plugin)
Here’s how to get Zomoc up and running in 3 steps in a Vite project.
1. Configure Vite & TypeScript
Add zomoc to your vite.config.ts and update tsconfig.json to recognize the virtual module.
vite.config.ts
// vite.config.ts
import { defineConfig } from "vite"
import zomoc from "zomoc/vite"
export default defineConfig({
plugins: [
zomoc(), // Add the plugin
],
})tsconfig.json
{
"compilerOptions": {
// ... other options
"types": ["zomoc/client"]
}
}2. Create a Mock and an Interface
Create a mock.json file and a corresponding interface.ts file.
src/api/mock.json
{
"GET /users": "IUserListResponse"
}src/api/interface.ts
export interface IUser {
id: string
name: string
email: string
}
export interface IUserListResponse {
users: IUser[]
total: number
}3. Activate the Interceptor
In your central axios configuration, set up the interceptor.
// src/shared/api/index.ts
import axios from "axios"
import { setupMockingInterceptor } from "zomoc"
import { finalSchemaUrlMap } from "virtual:zomoc" // Import from the virtual module
const api = axios.create({ baseURL: "https://api.example.com" })
// Zomoc will now intercept calls made with this instance
setupMockingInterceptor(api, {
enabled: import.meta.env.DEV, // Active only in development
registry: finalSchemaUrlMap,
debug: true, // Optional: log mocked requests
})
export { api }That's it! Now, when your app calls api.get('/users') in dev mode, Zomoc will serve a type-safe mock response.
Vite Plugin vs. CLI: Which Should I Use?
| Aspect | Vite Plugin | CLI |
| :------------------- | :------------------------ | :------------------------------------------- |
| Workflow | Fully Automatic (HMR) | Manual (generate) or Semi-Auto (--watch) |
| Setup | vite.config.ts | package.json scripts |
| Information Flow | Virtual Module (Abstract) | Physical File (Explicit) |
- If you are using a Vite project, use the Vite plugin. It offers the best developer experience.
- For Next.js, CRA, or any other environment, use the CLI. Using it with the
--watchoption is highly recommended.
🧪 Standalone Data Generation (for Storybook & Tests)
In addition to intercepting API requests, Zomoc provides a powerful feature to use its data generator directly in your code. This is extremely useful when you need type-safe mock data without an API call, such as for populating your Storybook stories or writing unit and integration tests.
✨ Important: How Scanning Works
Independent of
mock.json: This generator does not use themock.jsonfile at all. Instead, it directly scans theinterfacePathsdefined in your Zomoc configuration (zomoc.config.tsor Vite plugin options) to register all available interfaces into its registry."Survivor Principle" Scanning: Zomoc attempts to convert all interfaces from the specified paths into Zod schemas. However, if a conversion is not possible (e.g., due to complex generics not supported by
ts-to-zod, or importing types defined outside ofinterfacePaths), that interface is silently skipped. This prevents the entire process from halting and ensures the registry is built only with successfully converted "surviving" schemas. Therefore, if you find a specific type is missing from the generator, it's worth checking its definition.
The usage is simple: initialize the generator with the finalSchemaTypeMap provided by Zomoc, and then call it with the name of the interface you need.
Step 1: Import createGenerator and the Registry
First, import the createGenerator function from zomoc and the finalSchemaTypeMap from the location appropriate for your environment.
For Vite Users (Virtual Module):
import { createGenerator } from "zomoc"
import { finalSchemaTypeMap } from "virtual:zomoc"For CLI Users (Generated File):
// The import path for 'zomoc/cli' may vary based on your project structure.
import { createGenerator } from "zomoc/cli"
import { finalSchemaTypeMap } from "../.zomoc/registry" // Adjust the path to your generated file.Step 2: Create and Use the Generator
Once you have the necessary modules, the usage is identical in any environment.
// 1. Initialize the generator with the registry.
const getMock = createGenerator(finalSchemaTypeMap)
// 2. Generate data by passing the name of a scanned interface.
// The return value is perfectly type-inferred!
const mockUser = getMock("IUser")
const mockProduct = getMock("IProduct")
// Example usage in a Storybook story:
export default {
title: "Components/UserProfile",
component: UserProfile,
args: {
// Pass the generated mock data as a prop.
user: getMock("IUser"),
},
}Advanced Options:
You can control how data is generated by passing an options object as the second argument to the getMock function. This allows you to apply most of the advanced generation strategies available in mock.json, such as creating an array of objects (repeatCount) or simulating a paginated response. For more details on the available options, please refer to the In-Depth Guide.
// Create an array of 10 user objects
const userList = getMock("IUser", { repeatCount: 10 })
// Simulate a paginated response
// (assuming IProductResponse is shaped like { data: IProduct[], total: number })
const paginatedProducts = getMock("IProductResponse", {
pagination: {
itemsKey: "data", // The key for the data array
totalKey: "total", // The key for the total count
},
})This approach allows you to flexibly use the same high-quality, type-safe mock data engine that the interceptor uses, anywhere in your project.
📚 In-Depth Guide
This section covers advanced configuration and features.
Mocking Error Responses and Multiple Statuses
While the simple configuration is great for mocking successful "happy path" responses (200 OK), Zomoc also provides a powerful "Response Map Mode" to handle various HTTP status codes and error cases for a single API endpoint. This allows you to robustly test how your application behaves in different scenarios, such as when data is not found (404) or a server error occurs (500).
The core idea is to use a responses object where each key is an HTTP status code, and a top-level status property acts as a "switch" to determine which response is currently active.
1. Configure the responses map in your mock definition:
src/api/mock.json
{
"GET /api/user/:id": {
"status": 404,
"responses": {
"200": {
"responseType": "IUserProfile"
},
"404": {
"responseBody": { "message": "User not found" }
},
"500": {
"responseBody": { "error": "Internal Server Error" }
}
}
}
}status: This is the switch. The value here (e.g.,404) determines which of the definitions insideresponseswill be used. You can change this value to easily switch between testing a success, a not-found error, or a server error.responses: This object maps status codes to their respective response definitions."200": For a 200 status, Zomoc will generate data based on theIUserProfileinterface as usual."404"and"500": For these statuses, we useresponseBody.
2. Using responseBody for Direct Data Injection
The responseBody key allows you to bypass the schema-based data generation and return a specific, predefined JSON object directly. This is perfect for defining fixed error messages or testing edge-case data structures.
Note:
responseBodytakes precedence. If bothresponseBodyandresponseTypeare defined for the same response, the value inresponseBodywill always be used.responseBodycan only be used inside theresponsesmap.
3. AxiosError-Compliant Responses
When Zomoc serves a response with a status code of 400 or higher, it doesn't just resolve the promise with an error status. Instead, it rejects the promise with an AxiosError-like object. This object includes properties like isAxiosError: true and a response object containing the status, data, etc.
This is a critical feature because it means your application's error handling logic (e.g., try...catch blocks or react-query's onError callback) will work identically for both real API errors and mocked errors, ensuring your tests are realistic and reliable.
Pagination Mocking
Zomoc can intelligently mock paginated API responses. When a pagination configuration is provided for a specific endpoint, Zomoc will read the page and size from the request (query params or body) and generate the exact number of items requested.
1. Configure the pagination object in your mock definition:
In your mock.json, instead of just a type name string, use an object with a responseType and a pagination key.
src/api/mock.json
{
"GET /users": {
"responseType": "IUserListResponse",
"pagination": {
"itemsKey": "users",
"totalKey": "total",
"pageKey": "page",
"sizeKey": "size"
}
}
}itemsKey: The key in the response object that holds the array of items.totalKey: The key for the total number of items.pageKey: The name of the parameter for the page number (defaults to"page").sizeKey: The name of the parameter for the page size (defaults to"size").
If you don't provide these parameters in your API call, Zomoc will use default values (page: 1, size: 10).
2. Make an API call with pagination params:
// e.g., GET /users?page=1&size=10
api.get("/users", { params: { page: 1, size: 10 } })Zomoc will intercept this call and return a response where the users array contains exactly 10 mock items.
Note: If an endpoint is configured for pagination,
zomocwill generate the requested number of items. For regular array responses (likeIUser[]) that are not configured for pagination, it will automatically generate a random number of items (1-3) to make the data feel more dynamic.
Controlling Array Length
For non-paginated endpoints that return an array, you can specify the exact number of items to generate using the repeatCount option. If not provided, a random number of items (1-3) will be generated.
Unlike paginated responses which are objects containing an array, this feature is for APIs that directly return an array.
To use this, you must first explicitly define and export a type for your array in your interface file. This is because Zomoc needs to find a Zod schema that corresponds to an array (ZodArray) to apply the repeatCount logic correctly.
1. Define and export the array type:
interface.ts
export interface ITag {
id: number
name: string
}
// Explicitly define and export the array type.
export type ITagList = ITag[]2. Use the array type's name in mock.json:
mock.json
{
"GET /api/tags": {
"responseType": "ITagList",
"repeatCount": 5
}
}This configuration will always return an array containing exactly 5 mock ITag items.
Key Difference:
repeatCountvs.paginationIt's important to understand when to userepeatCountand when to usepagination.
- Use
repeatCountwhen the API response is an array itself. TheresponseTypemust be an array type (e.g.,type MyArray = Item[]).- Use
paginationwhen the API response is an object that contains an array. TheresponseTypemust be an object type.Applying
repeatCountto aresponseTypethat is an object will not affect the length of arrays inside that object. Those arrays will default to a random length of 1-3 items.
Putting It All Together: A Quick Guide
Here’s a summary table to help you decide which configuration to use based on your API's response shape.
| Your API Response... | Your Goal | mock.json Configuration | Result |
| :-------------------------------------------------------------- | :------------------------------- | :--------------------------- | :--------------------------------------------------- |
| Is an object with an array(e.g., { users: [], ... }) | Control array length via request | Use pagination object | Array length matches the size param in the request |
| Is an object with an array(e.g., { users: [], ... }) | Let Zomoc decide the length | Omit pagination object | Inner array gets a random length (1-3) |
| Is an array itself(e.g., [{}, {}]) | Set a fixed array length | Use repeatCount number | Array length matches repeatCount |
| Is an array itself(e.g., [{}, {}]) | Let Zomoc decide the length | Omit repeatCount number | Array gets a random length (1-3) |
Important: Do not use pagination for a responseType that is an array, and do not expect repeatCount to work on a responseType that is an object. Mixing them will lead to unexpected behavior or errors.
Mocking Strategy: Fixed vs. Random
For union types ('a' | 'b') or enum types in your interfaces, you can control how zomoc generates mock data. This is particularly useful for creating predictable tests.
1. Configure the mockingStrategy in your mock.json:
{
"GET /api/status": {
"responseType": "IStatus",
"mockingStrategy": "fixed"
}
}mockingStrategy: (Optional) Specifies how to generate data forZodUnionorZodEnumtypes."random"(default): Randomly selects one of the union/enum options. This is great for discovering edge cases."fixed": Always selects the first option defined in the union/enum. This is essential for creating predictable and stable tests where you need to rely on a specific value.
Example interface.ts:
export type Status = "Pending" | "Success" | "Failed"
export interface IStatus {
status: Status
}With mockingStrategy: "fixed", the status field will always be "Pending". With "random", it could be any of the three values.
URL Matching and Priority
Zomoc uses path-to-regexp for URL matching, which is the same engine used by frameworks like Express.js. This allows you to define dynamic URL paths.
Key Principles:
- Dynamic Segments: Use colons (
:) to define dynamic parts of a URL (e.g.,GET /api/users/:userId). - Query Strings: Query strings (
?key=value) are ignored during matching, so you only need to define the URL path. - Matching Order: Rules in
mock.jsonare evaluated from top to bottom. The first rule that matches the incoming request will be used, and the evaluation will stop.
This means the order of your rules is critical. More specific paths must always be placed before more general, dynamic paths.
Example of incorrect order:
{
"GET /api/users/:userId": "IUserProfile",
"GET /api/users/me": "IMyProfile"
}In this case, a request to /api/users/me will be incorrectly matched by the first rule (GET /api/users/:userId), with userId being "me".
Example of correct order:
{
"GET /api/users/me": "IMyProfile",
"GET /api/users/:userId": "IUserProfile"
}With this order, a request to /api/users/me is correctly caught by the first, more specific rule.
Customizing File Paths
By default, Zomoc searches for files matching **/mock.json, **/interface.ts, and **/type.ts. You can override these paths via the CLI or your Vite config.
CLI
You can specify custom paths by passing the --mock-paths and --interface-paths options to the generate command. Each option can accept multiple glob patterns separated by spaces.
# Pass two patterns to --mock-paths and one to --interface-paths
npx zomoc generate \
--mock-paths "src/mocks/api.json" "src/features/**/mock.json" \
--interface-paths "src/types/**/*.ts"Vite Plugin
In your vite.config.ts, you can pass the options directly to the plugin.
// vite.config.ts
import zomoc from "zomoc/vite"
export default {
plugins: [
zomoc({
// Glob patterns for your mock definition files.
mockPaths: ["src/features/**/mock.json"],
// Glob patterns for your TypeScript interface files.
interfacePaths: ["src/features/**/model/*.ts"],
}),
],
}Custom Data Generators
By default, zomoc generates simple placeholder data. For more realistic mocks, you can provide your own generator functions using the customGenerators option. This is powerful when combined with a library like @faker-js/faker.
1. First, install faker:
npm install -D @faker-js/faker2. Then, pass your custom generators to the interceptor:
// src/shared/api/index.ts
import { setupMockingInterceptor, type CustomGenerators } from "zomoc"
import { faker } from "@faker-js/faker"
// Define your custom generator functions
const customGenerators: CustomGenerators = {
string: (key) => {
if (key.toLowerCase().includes("email")) return faker.internet.email()
if (key.toLowerCase().includes("name")) return faker.person.fullName()
return faker.lorem.sentence()
},
number: () => faker.number.int({ max: 1000 }),
}
setupMockingInterceptor(api, {
// ...other options
customGenerators,
})With this setup, zomoc will call your functions to generate context-aware data.
Limitations
While Zomoc is a helpful tool, it has some limitations you should be aware of:
- Generic Types: Zomoc relies on
ts-to-zod
🤝 Contributing
Contributions are welcome! If you have a feature request, bug report, or pull request, please feel free to open an issue or PR.
License
This project is licensed under the MIT License.
