A powerful TypeScript code generation tool that automatically generates type-safe API client code based on OpenAPI specifications. It is designed for general use cases and is also deeply optimized for the Wow Domain-Driven Design framework, providing native support for the CQRS architectural pattern.
Wow Framework: A domain-driven design framework that provides event sourcing, CQRS (Command Query Responsibility Segregation), and aggregate patterns for building scalable distributed systems. This generator provides enhanced support for Wow's CQRS architecture while remaining compatible with standard REST APIs.
In modern front-end and back-end separation architecture, team collaboration efficiency is often the key to project success. Fetcher Generator fundamentally changes traditional front-end and back-end collaboration patterns through automated code generation, bringing significant efficiency improvements and quality assurance to teams.
- High Manual Update Costs: When back-end interfaces change, front-end needs to manually update multiple pieces of code, which is time-consuming and error-prone
- Complex Version Management: Different versions of APIs correspond to different client codes, with maintenance costs growing exponentially
- Surge in Communication Costs: Every interface adjustment requires repeated confirmation between front-end and back-end, affecting development rhythm
- Type Inconsistency Risks: Front-end and back-end maintain type definitions separately, with subtle differences causing runtime errors
- Documentation Lag Issues: API documentation becomes detached from actual code, making it difficult for newcomers to get started and reducing team knowledge transfer efficiency
- Inconsistent Code Standards: Manually written client code varies in style, increasing maintenance difficulty
- Repetitive Work: Writing similar request handling code for each interface wastes innovation time
- Long Debugging Cycles: Type mismatch issues are often discovered during integration testing, with high repair costs
- Limited Iteration Speed: Fear of breaking existing functionality leads to hesitation in refactoring and optimization
- Real-time Synchronization Mechanism: Automatically generates the latest client code based on OpenAPI specifications, ensuring complete synchronization with back-end APIs
- Single Source of Truth Guarantee: OpenAPI specifications serve as the sole authoritative source, completely eliminating front-end and back-end understanding deviations
- Rapid Change Response: One-click regeneration after API adjustments, completed in minutes
- Compile-time Type Checking: Captures interface call errors during the coding phase, preventing issues before they occur
- Standardized Code Output: Unified code style and best practices, improving maintainability
- Self-contained Documentation: Generated code comes with complete type hints and comments, reducing understanding costs
- Intelligent Code Completion: Complete type system provides precise code prompts, improving coding efficiency
- Semantic Method Naming: Intuitive API method names make code self-explanatory
- Structured Code Organization: Clear module divisions facilitate team collaboration and code review
Through actual project validation, using Generator can bring significant efficiency improvements to teams:
- 📈 Development efficiency improvement 30-50%
- Reduce 60%+ manual coding workload
- Save 70% interface debugging time
- Reduce 80% type-related bugs
- New member onboarding time shortened by 50%
- Code review efficiency improved by 40%
- Cross-team communication costs reduced by 60%
- API change response speed increased 5x
- Confidence and frequency of refactoring significantly increased
- Technical debt accumulation rate slowed
For Technical Teams
- Release developer creativity, focus on business innovation rather than repetitive work
- Establish reliable technical infrastructure to support rapid business iteration
- Improve code quality and maintainability, reduce long-term maintenance costs
For Product Delivery
- Accelerate product iteration cycles, respond faster to market changes
- Improve delivery quality, enhance user satisfaction
- Reduce project risks, ensure on-time delivery
For Organizational Efficiency
- Optimize team collaboration patterns, improve overall development efficiency
- Standardize development processes, promote best practice dissemination
- Build scalable technical architecture to support business continuous growth
Fetcher Generator is not just a technical tool, but an innovation in team collaboration patterns. Through automated and standardized approaches, it makes front-end and back-end collaboration more efficient and reliable, providing a solid foundation for the rapid iteration and high-quality delivery of digital products.
- 🎯 OpenAPI 3.0+ Support: Full support for OpenAPI 3.0+ specifications (JSON/YAML)
- 📦 TypeScript Code Generation: Generates type-safe TypeScript interfaces, enums, and classes
- 🔧 CLI Tool: Easy-to-use command-line interface for code generation
- 🎨 Decorator-Based APIs: Generates decorator-based client classes for clean API interactions
- 📋 Comprehensive Models: Handles complex schemas including unions, intersections, enums, and references
- 🚀 Fetcher Integration: Seamlessly integrates with the Fetcher ecosystem packages
- 📊 Progress Logging: Friendly logging with progress indicators during generation
- 📁 Auto Index Generation: Automatically generates index.ts files for clean module organization
- 🌐 Remote Spec Support: Load OpenAPI specs directly from HTTP/HTTPS URLs
- 🎭 Event Streaming: Generates both regular and event-stream command clients
- 🏗️ Domain-Driven Design Support: Specialized support for Wow framework with aggregates, commands, queries, and events (CQRS patterns)
# Using npm
npm install -g @ahoo-wang/fetcher-generator
# Using pnpm
pnpm add -g @ahoo-wang/fetcher-generator
# Using yarn
yarn global add @ahoo-wang/fetcher-generator# Generate TypeScript code from OpenAPI spec
fetcher-generator generate -i ./openapi-spec.json -o ./generatedfetcher-generator generate [options]-i, --input <path>: Input OpenAPI specification file path or URL (required)- Supports local file paths (e.g.,
./api-spec.json,/path/to/spec.yaml) - Supports HTTP/HTTPS URLs (e.g.,
https://api.example.com/openapi.json)
- Supports local file paths (e.g.,
-o, --output <path>: Output directory path (default:src/generated)-c, --config <file>: Configuration file path (optional)-h, --help: Display help information-v, --version: Display version number
# Generate code from a local OpenAPI JSON file
fetcher-generator generate -i ./api-spec.json -o ./src/generated
# Generate code from a YAML specification
fetcher-generator generate -i ./api-spec.yaml -o ./src/generated
# Generate code from a remote OpenAPI specification via HTTPS
fetcher-generator generate -i https://api.example.com/openapi.json -o ./src/generated
# Generate code from a remote YAML specification via HTTP
fetcher-generator generate -i http://localhost:8080/api-spec.yaml -o ./src/generatedThe generator creates the following structure in your output directory:
output/
├── {bounded-context}/
│ ├── index.ts # Auto-generated index file exporting all aggregates and API clients
│ ├── boundedContext.ts # Bounded context alias constant
│ ├── types.ts # Shared types for the bounded context
│ ├── {Tag}ApiClient.ts # API client classes for custom endpoints (one per OpenAPI tag)
│ └── {aggregate}/ # Aggregate-specific files
│ ├── index.ts # Auto-generated index file for aggregate
│ ├── types.ts # Aggregate-specific types, models, and enums
│ ├── queryClient.ts # Query client factory for state and event queries
│ └── commandClient.ts # Command client classes (regular and streaming)
├── index.ts # Root index file exporting all bounded contexts
└── tsconfig.json # TypeScript configuration for generated code
The generator automatically creates index.ts files in all directories to provide convenient module exports:
- Root index.ts: Exports all bounded contexts
- Bounded context index.ts: Exports all aggregates and API clients (based on OpenAPI tags) within the context
- Aggregate index.ts: Exports all files within the aggregate
This allows for clean imports like:
// Import everything from a bounded context
import * as example from './generated/example';
// Import specific aggregates and API clients (API clients are generated per OpenAPI tag)
import { cart, CartApiClient } from './generated/example';
// Import specific files
import { CartState } from './generated/example/cart';/** apply_execution_failed */
export interface ApplyExecutionFailed {
error: ErrorDetails;
executeAt: number;
recoverable: RecoverableType | undefined;
}
/** apply_execution_success */
export interface ApplyExecutionSuccess {
executeAt: number;
}
/** execution_failed_status */
export enum ExecutionFailedStatus {
PREPARED = 'PREPARED',
SUCCEEDED = 'SUCCEEDED',
FAILED = 'FAILED',
COMPENSATED = 'COMPENSATED',
}// Generated query client factory for domain-driven design
import {
QueryClientFactory,
QueryClientOptions,
ResourceAttributionPathSpec,
} from '@ahoo-wang/fetcher-wow';
import {
CartAggregatedFields,
CartItemAdded,
CartItemRemoved,
CartQuantityChanged,
CartState,
} from './types';
const DEFAULT_QUERY_CLIENT_OPTIONS: QueryClientOptions = {
contextAlias: 'example',
aggregateName: 'cart',
resourceAttribution: ResourceAttributionPathSpec.OWNER,
};
type DOMAIN_EVENT_TYPES = CartItemAdded | CartItemRemoved | CartQuantityChanged;
export const cartQueryClientFactory = new QueryClientFactory<
CartState,
CartAggregatedFields | string,
DOMAIN_EVENT_TYPES
>(DEFAULT_QUERY_CLIENT_OPTIONS);// Generated command client with decorator-based API
import { ContentTypeValues } from '@ahoo-wang/fetcher';
import {
type ApiMetadata,
type ApiMetadataCapable,
api,
attribute,
autoGeneratedError,
del,
path,
post,
put,
request,
} from '@ahoo-wang/fetcher-decorator';
import { JsonEventStreamResultExtractor } from '@ahoo-wang/fetcher-eventstream';
import type {
CommandRequest,
CommandResult,
CommandResultEventStream,
DeleteAggregate,
RecoverAggregate,
} from '@ahoo-wang/fetcher-wow';
import {
AddCartItem,
ChangeQuantity,
MockVariableCommand,
MountedCommand,
RemoveCartItem,
ViewCart,
} from './types';
enum COMMAND_ENDPOINT_PATHS {
VIEW_CART = '/owner/{ownerId}/cart/view_cart',
ADD_CART_ITEM = '/owner/{ownerId}/cart/add_cart_item',
CHANGE_QUANTITY = '/owner/{ownerId}/cart/change_quantity',
REMOVE_CART_ITEM = '/owner/{ownerId}/cart/remove_cart_item',
MOUNTED_COMMAND = '/owner/{ownerId}/cart/mounted_command',
MOCK_VARIABLE_COMMAND = '/tenant/{tenantId}/owner/{ownerId}/cart/{id}/{customerId}/{mockEnum}',
DEFAULT_DELETE_AGGREGATE = '/owner/{ownerId}/cart',
DEFAULT_RECOVER_AGGREGATE = '/owner/{ownerId}/cart/recover',
}
const DEFAULT_COMMAND_CLIENT_OPTIONS: ApiMetadata = {
basePath: 'example',
};
@api()
export class CartCommandClient implements ApiMetadataCapable {
constructor(
public readonly apiMetadata: ApiMetadata = DEFAULT_COMMAND_CLIENT_OPTIONS,
) {}
/** view_cart */
@put(COMMAND_ENDPOINT_PATHS.VIEW_CART)
viewCart(
@request() commandRequest: CommandRequest<ViewCart>,
@attribute() attributes: Record<string, any>,
): Promise<CommandResult> {
throw autoGeneratedError(commandRequest, attributes);
}
/**
* 加入购物车
* 加入购物车
*/
@post(COMMAND_ENDPOINT_PATHS.ADD_CART_ITEM)
addCartItem(
@request() commandRequest: CommandRequest<AddCartItem>,
@attribute() attributes: Record<string, any>,
): Promise<CommandResult> {
throw autoGeneratedError(commandRequest, attributes);
}
/** 变更购买数量 */
@put(COMMAND_ENDPOINT_PATHS.CHANGE_QUANTITY)
changeQuantity(
@request() commandRequest: CommandRequest<ChangeQuantity>,
@attribute() attributes: Record<string, any>,
): Promise<CommandResult> {
throw autoGeneratedError(commandRequest, attributes);
}
/** 删除商品 */
@put(COMMAND_ENDPOINT_PATHS.REMOVE_CART_ITEM)
removeCartItem(
@request() commandRequest: CommandRequest<RemoveCartItem>,
@attribute() attributes: Record<string, any>,
): Promise<CommandResult> {
throw autoGeneratedError(commandRequest, attributes);
}
}The generator also creates streaming command clients for event-driven interactions:
@api('', {
headers: { Accept: ContentTypeValues.TEXT_EVENT_STREAM },
resultExtractor: JsonEventStreamResultExtractor,
})
export class CartStreamCommandClient implements ApiMetadataCapable {
constructor(
public readonly apiMetadata: ApiMetadata = DEFAULT_COMMAND_CLIENT_OPTIONS,
) {}
/** view_cart */
@put(COMMAND_ENDPOINT_PATHS.VIEW_CART)
viewCart(
@request() commandRequest: CommandRequest<ViewCart>,
@attribute() attributes: Record<string, any>,
): Promise<CommandResultEventStream> {
throw autoGeneratedError(commandRequest, attributes);
}
// ... other streaming methods
}The generator also creates API client classes for custom endpoints that don't follow the domain-driven command pattern. These are generated based on OpenAPI tags (one client class per tag):
// Generated API client for custom endpoints
import {
type ApiMetadata,
type ApiMetadataCapable,
ParameterRequest,
api,
attribute,
autoGeneratedError,
get,
path,
post,
request,
} from '@ahoo-wang/fetcher-decorator';
import { CommandResult } from '@ahoo-wang/fetcher-wow';
import { CartData } from './cart/types';
import { ContentTypeValues } from '@ahoo-wang/fetcher';
import {
JsonEventStreamResultExtractor,
JsonServerSentEventStream,
} from '@ahoo-wang/fetcher-eventstream';
/** Shopping Cart */
@api()
export class CartApiClient implements ApiMetadataCapable {
constructor(
public readonly apiMetadata: ApiMetadata = { basePath: 'example' },
) {}
/** Custom command sending */
@post('/cart/{userId}/customize-send-cmd')
customizeSendCmd(
@path('userId') userId: string,
@request() httpRequest?: ParameterRequest,
@attribute() attributes?: Record<string, any>,
): Promise<CommandResult> {
throw autoGeneratedError(userId, httpRequest, attributes);
}
/** Add cart item with event streaming */
@post('/cart/{userId}/add-cart-item', {
headers: { Accept: ContentTypeValues.TEXT_EVENT_STREAM },
resultExtractor: JsonEventStreamResultExtractor,
})
addCartItem(
@path('userId') userId: string,
@request() httpRequest?: ParameterRequest,
@attribute() attributes?: Record<string, any>,
): Promise<JsonServerSentEventStream<CommandResult>> {
throw autoGeneratedError(userId, httpRequest, attributes);
}
/** Get current user's cart */
@get('/cart/me')
me(): Promise<CartData> {
throw autoGeneratedError();
}
/** Get current user's cart (sync) */
@get('/cart/me/sync')
meSync(): Promise<CartData> {
throw autoGeneratedError();
}
}The generated code is designed to work seamlessly with the Fetcher ecosystem:
import { fetcher, Fetcher } from '@ahoo-wang/fetcher';
import { all } from '@ahoo-wang/fetcher-wow';
import { cartQueryClientFactory } from './generated/example/cart/queryClient';
import { CartCommandClient } from './generated/example/cart/commandClient';
import { CartApiClient } from './generated/example/CartApiClient';
// Create a fetcher instance
const fetcher = new Fetcher({
baseURL: 'https://api.example.com',
});
// Use the generated query client factory
const snapshotClient = cartQueryClientFactory.createSnapshotQueryClient({
fetcher: fetcher,
});
const cartState = await snapshotClient.singleState({ condition: all() });
// Use the generated command client
const commandClient = new CartCommandClient({ fetcher: fetcher });
const result = await commandClient.addCartItem(
{
command: {
productId: 'product-123',
quantity: 2,
},
},
{
ownerId: 'user-456',
},
);
// Use the generated API client for custom endpoints (based on OpenAPI tag "cart")
const apiClient = new CartApiClient({ fetcher: fetcher });
const cartData = await apiClient.me();Create a .fetcherrc.json configuration file for advanced generation options:
{
"generator": {
"targetFramework": "wow",
"outputFormat": "typescript",
"basePath": "api/v1",
"generateIndexFiles": true,
"verbose": true,
"typeMappings": {
"string": "string",
"integer": "number",
"boolean": "boolean",
"number": "number"
},
"schemaTransformers": [
{
"pattern": "^wow\\.",
"transform": "removePrefix"
}
]
}
}Generate code for multiple bounded contexts from a single OpenAPI spec:
# Generate all contexts
fetcher-generator generate -i ./multi-context-api.json -o ./src/generated
# Generated structure:
# src/generated/
# ├── ecommerce/
# │ ├── index.ts
# │ ├── boundedContext.ts
# │ ├── cart/
# │ ├── order/
# │ └── product/
# └── inventory/
# ├── index.ts
# ├── boundedContext.ts
# └── stock/Generated code supports event streaming for real-time updates:
import { cartQueryClientFactory } from './generated/ecommerce/cart/queryClient';
import { CartStreamCommandClient } from './generated/ecommerce/cart/commandClient';
// Create streaming query client
const eventClient = cartQueryClientFactory.createEventQueryClient({
fetcher: fetcher,
onEvent: event => {
console.log('Cart event:', event);
// Handle real-time cart updates
},
});
// Use streaming command client
const streamCommandClient = new CartStreamCommandClient({ fetcher });
const eventStream = await streamCommandClient.addCartItem(
{
command: { productId: 'item-123', quantity: 1 },
},
{ ownerId: 'user-456' },
);
// Process streaming events
for await (const event of eventStream) {
console.log('Command event:', event);
}Generated code includes runtime type validation:
import { CartState, isCartState } from './generated/ecommerce/cart/types';
// Runtime type checking
function processCartData(data: unknown) {
if (isCartState(data)) {
// TypeScript knows data is CartState here
console.log('Valid cart state:', data.items.length);
} else {
throw new Error('Invalid cart data');
}
}Use generated clients with popular state management libraries:
// Redux Toolkit integration
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { cartQueryClientFactory } from './generated/ecommerce/cart/queryClient';
const fetchCartState = createAsyncThunk(
'cart/fetchState',
async (ownerId: string) => {
const snapshotClient = cartQueryClientFactory.createSnapshotQueryClient({
fetcher: fetcher,
});
return await snapshotClient.singleState({ condition: { ownerId } });
},
);
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], loading: false },
reducers: {},
extraReducers: builder => {
builder.addCase(fetchCartState.pending, state => {
state.loading = true;
});
builder.addCase(fetchCartState.fulfilled, (state, action) => {
state.items = action.payload.items;
state.loading = false;
});
},
});Generated clients work seamlessly with retry mechanisms:
import { Fetcher } from '@ahoo-wang/fetcher';
import { CartCommandClient } from './generated/ecommerce/cart/commandClient';
// Configure fetcher with retry logic
const fetcher = new Fetcher({
baseURL: 'https://api.example.com',
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryCondition: error => error.status >= 500,
},
});
const commandClient = new CartCommandClient({ fetcher });
// Commands automatically retry on server errors
try {
const result = await commandClient.addCartItem(
{
command: { productId: 'item-123', quantity: 1 },
},
{ ownerId: 'user-456' },
);
} catch (error) {
console.error('Failed to add item after retries:', error);
}Write unit tests for generated clients:
import { describe, it, expect, vi } from 'vitest';
import { CartCommandClient } from './generated/ecommerce/cart/commandClient';
describe('CartCommandClient', () => {
it('should add item to cart', async () => {
const mockFetcher = {
request: vi.fn().mockResolvedValue({ success: true }),
};
const client = new CartCommandClient({
fetcher: mockFetcher as any,
});
// This will throw autoGeneratedError in generated code
// In real usage, you'd use a decorator interceptor
expect(() =>
client.addCartItem(
{
command: { productId: 'test', quantity: 1 },
},
{ ownerId: 'user' },
),
).toThrow();
});
});The generator expects OpenAPI 3.0+ specifications with specific patterns for WOW domain-driven design framework:
Aggregates are identified by operation tags that follow the pattern:
{context}.{aggregate}
The generator recognizes operations by their operationId suffixes:
- State Snapshots: Operations ending with
.snapshot_state.single - Event Queries: Operations ending with
.event.list_query - Field Queries: Operations ending with
.snapshot.count - Commands: Any operation with a valid command request/response structure
- Commands: Operations with
POST,PUT,DELETEmethods that returnwow.CommandOkresponses - Queries: Operations with
GETmethod for retrieving aggregate state or events - Events: Operations returning event stream arrays with domain event structures
- Use descriptive names for schemas
- Avoid
wow.prefixed schemas (reserved for internal framework schemas) - Command request bodies should reference schemas in
components/schemas - State and event schemas should follow the expected structure for domain modeling
# Build the package
pnpm build
# Run tests
pnpm test
# Run linting
pnpm lint# Generate test output using the demo spec
pnpm generate
# Run tests
pnpm test
# Run tests with coverage
pnpm test -- --coverageContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.