UNPKG

zomoc

Version:

A type-safe API mocking tool for frontend development, powered by axios and Zod.

568 lines (399 loc) โ€ข 22 kB
[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 `@faker-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 [`@faker-js/faker`](https://fakerjs.dev/). **1. First, install faker:** ```bash npm install -D @faker-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.