zomoc
Version:
A type-safe API mocking tool for frontend development, powered by axios and Zod.
568 lines (399 loc) โข 22 kB
Markdown
[English](./README.md) | [ํ๊ตญ์ด](./README.ko.md)
---
# 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/:id` out 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 `-js/faker`) for more realistic mock data.
## Installation
```bash
npm install -D zomoc
```
`zomoc` 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`**
```json
{
"GET /users": "IUserListResponse"
}
```
**`src/api/interface.ts`**
```typescript
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.
```bash
npx zomoc generate
```
This 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.json` to run `npx zomoc generate --watch` alongside your `dev` command. 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.
```typescript
// 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`**
```typescript
// vite.config.ts
import { defineConfig } from "vite"
import zomoc from "zomoc/vite"
export default defineConfig({
plugins: [
zomoc(), // Add the plugin
],
})
```
**`tsconfig.json`**
```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`**
```json
{
"GET /users": "IUserListResponse"
}
```
**`src/api/interface.ts`**
```typescript
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.
```typescript
// 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 `--watch` option 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 the `mock.json` file at all. Instead, it directly scans the `interfacePaths` defined in your Zomoc configuration (`zomoc.config.ts` or 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 of `interfacePaths`), 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):**
```typescript
import { createGenerator } from "zomoc"
import { finalSchemaTypeMap } from "virtual:zomoc"
```
**For CLI Users (Generated File):**
```typescript
// 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.
```typescript
// 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](#-in-depth-guide).
```typescript
// 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`**
```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 inside `responses` will 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 the `IUserProfile` interface as usual.
- `"404"` and `"500"`: For these statuses, we use `responseBody`.
**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:** `responseBody` takes precedence. If both `responseBody` and `responseType` are defined for the same response, the value in `responseBody` will always be used. `responseBody` can only be used inside the `responses` map.
**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`**
```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:**
```typescript
// 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, `zomoc` will generate the requested number of items. For regular array responses (like `IUser[]`) 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`**
```typescript
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`**
```json
{
"GET /api/tags": {
"responseType": "ITagList",
"repeatCount": 5
}
}
```
This configuration will always return an array containing exactly 5 mock `ITag` items.
> **Key Difference: `repeatCount` vs. `pagination`**
> It's important to understand when to use `repeatCount` and when to use `pagination`.
>
> - **Use `repeatCount`** when the API response **is an array itself**. The `responseType` must be an array type (e.g., `type MyArray = Item[]`).
> - **Use `pagination`** when the API response **is an object that contains an array**. The `responseType` must be an object type.
>
> Applying `repeatCount` to a `responseType` that 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**<br/>(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**<br/>(e.g., `{ users: [], ... }`) | Let Zomoc decide the length | Omit `pagination` object | Inner array gets a random length (1-3) |
| **Is an array itself**<br/>(e.g., `[{}, {}]`) | Set a fixed array length | Use **`repeatCount`** number | Array length matches `repeatCount` |
| **Is an array itself**<br/>(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`:**
```json
{
"GET /api/status": {
"responseType": "IStatus",
"mockingStrategy": "fixed"
}
}
```
- **`mockingStrategy`**: (Optional) Specifies how to generate data for `ZodUnion` or `ZodEnum` types.
- `"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`:**
```typescript
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:**
1. **Dynamic Segments:** Use colons (`:`) to define dynamic parts of a URL (e.g., `GET /api/users/:userId`).
2. **Query Strings:** Query strings (`?key=value`) are ignored during matching, so you only need to define the URL path.
3. **Matching Order:** Rules in `mock.json` are 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:**
```json
{
"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:**
```json
{
"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.
```bash
# 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.
```typescript
// 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 [`-js/faker`](https://fakerjs.dev/).
**1. First, install faker:**
```bash
npm install -D -js/faker
```
**2. Then, pass your custom generators to the interceptor:**
```typescript
// 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.