@ahoo-wang/fetcher-decorator
The @ahoo-wang/fetcher-decorator package enables declarative API service definitions using TypeScript decorators. Instead of writing imperative HTTP calls, you define API classes with decorator annotations and the framework generates the implementation automatically at runtime.
Source: packages/decorator/src/
Installation
pnpm add @ahoo-wang/fetcher-decorator reflect-metadataWARNING
reflect-metadata is required as a peer dependency. It must be imported once at your application entry point before using decorators:
import 'reflect-metadata';Architecture
graph TB
subgraph sg_1 ["Decorator Layer"]
API["@api() class decorator"]
EP["@get/@post/@put/@delete/@patch<br>endpoint decorators"]
PARAM["@path/@query/@header/@body/@request/@attribute<br>parameter decorators"]
end
subgraph sg_2 ["Metadata Layer"]
AM["ApiMetadata<br>Class-level config"]
EM["EndpointMetadata<br>Method-level config"]
PM["ParameterMetadata<br>Argument bindings"]
FM["FunctionMetadata<br>Merged config"]
end
subgraph sg_3 ["Execution Layer"]
RE["RequestExecutor<br>Builds and runs the request"]
LIFE["ExecuteLifeCycle<br>beforeExecute / afterExecute"]
FETCHER["@ahoo-wang/fetcher<br>Interceptor pipeline"]
end
API --> AM
EP --> EM
PARAM --> PM
AM --> FM
EM --> FM
PM --> FM
FM --> RE
RE --> LIFE
RE --> FETCHER
style API fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style EP fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style PARAM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style AM fill:#161b22,stroke:#30363d,color:#e6edf3
style EM fill:#161b22,stroke:#30363d,color:#e6edf3
style PM fill:#161b22,stroke:#30363d,color:#e6edf3
style FM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style RE fill:#161b22,stroke:#30363d,color:#e6edf3
style LIFE fill:#161b22,stroke:#30363d,color:#e6edf3
style FETCHER fill:#161b22,stroke:#30363d,color:#e6edf3Decorator Execution Flow
When a decorated method is called, the framework executes a well-defined pipeline:
sequenceDiagram
autonumber
participant App as Application
participant Method as Decorated Method
participant RE as RequestExecutor
participant FM as FunctionMetadata
participant Fetcher as Fetcher
participant LC as ExecuteLifeCycle
participant IM as Interceptor Chain
App->>Method: userService.getUser(123)
Method->>RE: execute([123])
RE->>FM: resolveExchangeInit(args)
FM->>FM: Map @path params to path: {id: 123}
FM->>FM: Map @query params to query: {...}
FM->>FM: Map @header params to headers: {...}
FM->>FM: Map @body to body: {...}
FM->>FM: Resolve attributes, timeout, fetcher
FM-->>RE: {request, attributes}
RE->>Fetcher: resolveExchange(request, options)
Fetcher-->>RE: FetchExchange
alt Target implements ExecuteLifeCycle
RE->>LC: beforeExecute(exchange)
LC-->>RE: Modified exchange
end
RE->>IM: interceptors.exchange(exchange)
IM-->>RE: Completed exchange
alt Target implements ExecuteLifeCycle
RE->>LC: afterExecute(exchange)
end
RE->>RE: extractResult()
RE-->>App: Result (Response, JSON, etc.)Defining an API Class
The @api Decorator
The @api class decorator sets base path, headers, timeout, and the fetcher instance for all methods in the class. (apiDecorator.ts:232)
import { api, get, post, put, del, path, query, body, header, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import 'reflect-metadata';
interface User {
id: number;
name: string;
email: string;
}
@api('/api/v1/users', {
headers: { 'X-Api-Version': '1.0' },
timeout: 10000,
fetcher: 'api', // Use the 'api' named fetcher
})
class UserService {
@get('/')
getUsers(
@query('page') page: number,
@query('limit') limit: number,
): Promise<User[]> {
throw autoGeneratedError();
}
@get('/:id')
getUser(@path('id') id: number): Promise<User> {
throw autoGeneratedError();
}
@post('/')
createUser(@body() user: Omit<User, 'id'>): Promise<User> {
throw autoGeneratedError();
}
@put('/:id')
updateUser(
@path('id') id: number,
@body() user: Partial<User>,
): Promise<User> {
throw autoGeneratedError();
}
@del('/:id')
deleteUser(@path('id') id: number): Promise<void> {
throw autoGeneratedError();
}
}Endpoint Decorators
| Decorator | HTTP Method | Source |
|---|---|---|
@get(path?, metadata?) | GET | endpointDecorator.ts:101 |
@post(path?, metadata?) | POST | endpointDecorator.ts:126 |
@put(path?, metadata?) | PUT | endpointDecorator.ts:151 |
@del(path?, metadata?) | DELETE | endpointDecorator.ts:176 |
@patch(path?, metadata?) | PATCH | endpointDecorator.ts:201 |
@head(path?, metadata?) | HEAD | endpointDecorator.ts:229 |
@options(path?, metadata?) | OPTIONS | endpointDecorator.ts:254 |
Each endpoint decorator accepts optional MethodEndpointMetadata to override class-level settings:
@get('/special', {
headers: { 'X-Special': 'true' },
timeout: 30000,
resultExtractor: ResultExtractors.Json,
})
specialEndpoint(): Promise<any> {
throw autoGeneratedError();
}Parameter Decorators
Parameter decorators map function arguments to HTTP request components. When no explicit name is provided, the parameter name is automatically extracted from the function signature. (parameterDecorator.ts:199)
| Decorator | Type | Maps To | Object Expansion |
|---|---|---|---|
@path(name?) | ParameterType.PATH | URL path segment | Yes - object keys become path params |
@query(name?) | ParameterType.QUERY | URL query string | Yes - object keys become query params |
@header(name?) | ParameterType.HEADER | Request headers | Yes - object keys become headers |
@body() | ParameterType.BODY | Request body | No |
@request() | ParameterType.REQUEST | Base request object | No |
@attribute(name?) | ParameterType.ATTRIBUTE | Exchange attributes | Yes - objects and Maps |
graph LR
subgraph sg_1 ["Method Arguments"]
A1["id: number"]
A2["filters: object"]
A3["auth: string"]
A4["userData: User"]
A5["request: ParameterRequest"]
A6["attr: Map"]
end
subgraph sg_2 ["Parameter Decorators"]
D1["@path('id')"]
D2["@query()"]
D3["@header('Authorization')"]
D4["@body()"]
D5["@request()"]
D6["@attribute()"]
end
subgraph sg_3 ["Request Components"]
R1["urlParams.path = {id: 123}"]
R2["urlParams.query = {...filters}"]
R3["headers['Authorization'] = auth"]
R4["body = userData"]
R5["Merged into request"]
R6["exchange.attributes"]
end
A1 --> D1 --> R1
A2 --> D2 --> R2
A3 --> D3 --> R3
A4 --> D4 --> R4
A5 --> D5 --> R5
A6 --> D6 --> R6
style A1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style A2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style A3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style A4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style A5 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style A6 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style D1 fill:#161b22,stroke:#30363d,color:#e6edf3
style D2 fill:#161b22,stroke:#30363d,color:#e6edf3
style D3 fill:#161b22,stroke:#30363d,color:#e6edf3
style D4 fill:#161b22,stroke:#30363d,color:#e6edf3
style D5 fill:#161b22,stroke:#30363d,color:#e6edf3
style D6 fill:#161b22,stroke:#30363d,color:#e6edf3
style R1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style R2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style R3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style R4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style R5 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style R6 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3Object Parameter Expansion
Path, query, and header decorators support object arguments. When an object is passed, its key-value pairs are expanded into individual parameters:
@api('/api')
class SearchService {
@get('/search')
search(
@query() filters: { limit: number; offset: number; sort: string },
): Promise<SearchResult[]> {
throw autoGeneratedError();
}
}
// Calling:
service.search({ limit: 10, offset: 20, sort: 'name' });
// => GET /api/search?limit=10&offset=20&sort=nameThe @request Decorator
The @request() decorator allows passing a ParameterRequest object for full control over the request. It is merged with endpoint-level configuration, with the parameter request taking precedence. (parameterDecorator.ts:372)
@api('/api/users')
class UserService {
@post('/')
createUser(@request() req: ParameterRequest): Promise<User> {
throw autoGeneratedError();
}
}
// Usage:
service.createUser({
path: '/api/users',
headers: { 'X-Idempotency-Key': 'abc123' },
body: { name: 'John' },
timeout: 30000,
});The @attribute Decorator
The @attribute() decorator passes data to the exchange attributes, which can be read by any interceptor in the pipeline. (parameterDecorator.ts:408)
@api('/api/orders')
class OrderService {
@post('/')
createOrder(
@body() order: Order,
@attribute('tenantId') tenantId: string,
): Promise<Order> {
throw autoGeneratedError();
}
}Lifecycle Hooks (ExecuteLifeCycle)
Classes can implement the ExecuteLifeCycle interface to hook into the request execution pipeline. The OpenAI package uses this to dynamically switch result extractors based on whether streaming is enabled. (executeLifeCycle.ts:23)
graph TD
START["Method Called"] --> BUILD["Build FetchExchange"]
BUILD --> BEFORE["beforeExecute(exchange)"]
BEFORE --> INTERCEPT["Interceptor Chain"]
INTERCEPT --> AFTER["afterExecute(exchange)"]
AFTER --> RESULT["extractResult()"]
RESULT --> RETURN["Return to caller"]
style START fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style BUILD fill:#161b22,stroke:#30363d,color:#e6edf3
style BEFORE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style INTERCEPT fill:#161b22,stroke:#30363d,color:#e6edf3
style AFTER fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style RESULT fill:#161b22,stroke:#30363d,color:#e6edf3
style RETURN fill:#2d333b,stroke:#6d5dfc,color:#e6edf3import { api, get, ExecuteLifeCycle, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import type { FetchExchange } from '@ahoo-wang/fetcher';
@api('/api/data')
class DataService implements ExecuteLifeCycle {
async beforeExecute(exchange: FetchExchange): Promise<void> {
// Add tenant ID from session
exchange.ensureRequestHeaders()['X-Tenant-Id'] = getTenantId();
// Add request tracking
exchange.attributes.set('requestId', crypto.randomUUID());
}
async afterExecute(exchange: FetchExchange): Promise<void> {
// Log completion
const requestId = exchange.attributes.get('requestId');
console.log(`Request ${requestId} completed: ${exchange.response?.status}`);
}
@get('/items')
getItems(): Promise<Item[]> {
throw autoGeneratedError();
}
}EndpointReturnType
By default, decorated methods return the extracted result (e.g., parsed JSON). You can change this behavior to return the entire FetchExchange instead. (endpointReturnTypeCapable.ts:14)
| Value | Description |
|---|---|
EndpointReturnType.RESULT | Returns the extracted result (default) |
EndpointReturnType.EXCHANGE | Returns the full FetchExchange object |
import { EndpointReturnType } from '@ahoo-wang/fetcher-decorator';
@api('/api/users', { returnType: EndpointReturnType.EXCHANGE })
class UserService {
@get('/')
getUsers(): Promise<FetchExchange> {
throw autoGeneratedError();
}
}Metadata Resolution
The FunctionMetadata class merges API-level, endpoint-level, and parameter metadata into a single resolved configuration. Endpoint-level values override API-level values, and parameter decorator values override both. (functionMetadata.ts:43)
graph TD
subgraph sg_1 ["Priority (Highest to Lowest)"]
P1["Parameter Decorators<br>@path, @query, @header, @body"]
P2["Endpoint Metadata<br>@get path, metadata arg"]
P3["API Metadata<br>@api basePath, options"]
end
P1 --> MERGE["FunctionMetadata<br>resolveExchangeInit()"]
P2 --> MERGE
P3 --> MERGE
MERGE --> FINAL["Final FetchRequest"]
style P1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style P2 fill:#161b22,stroke:#30363d,color:#e6edf3
style P3 fill:#161b22,stroke:#30363d,color:#e6edf3
style MERGE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style FINAL fill:#161b22,stroke:#30363d,color:#e6edf3autoGeneratedError
The autoGeneratedError() function creates a placeholder error that satisfies ESLint's no-unused-vars rule while indicating the method body is replaced at runtime. (generated.ts:41)
import { autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
@get('/users/:id')
getUser(@path('id') id: number): Promise<User> {
throw autoGeneratedError(id); // Arguments are accepted but ignored
}Complete Example
import 'reflect-metadata';
import { api, get, post, del, path, query, body, header, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import { NamedFetcher, ResultExtractors } from '@ahoo-wang/fetcher';
// Setup: create a named fetcher
new NamedFetcher('myApi', {
baseURL: 'https://api.example.com',
headers: { 'Accept': 'application/json' },
timeout: 5000,
});
interface Product {
id: string;
name: string;
price: number;
}
interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
}
@api('/api/v2/products', { fetcher: 'myApi' })
class ProductService {
@get('/')
listProducts(
@query() filters: ProductFilters,
@query('page') page: number = 1,
@query('limit') limit: number = 20,
): Promise<Product[]> {
throw autoGeneratedError();
}
@get('/:id')
getProduct(
@path('id') id: string,
@header('Accept-Language') locale: string = 'en',
): Promise<Product> {
throw autoGeneratedError();
}
@post('/')
createProduct(@body() product: Omit<Product, 'id'>): Promise<Product> {
throw autoGeneratedError();
}
@del('/:id')
deleteProduct(@path('id') id: string): Promise<void> {
throw autoGeneratedError();
}
}
// Usage
const products = new ProductService();
const items = await products.listProducts({ category: 'electronics' }, 1, 10);Exported API Summary
| Export | Type | Source |
|---|---|---|
api | Decorator | apiDecorator.ts |
get, post, put, del, patch, head, options | Decorators | endpointDecorator.ts |
path, query, header, body, request, attribute | Decorators | parameterDecorator.ts |
ApiMetadata | Interface | apiDecorator.ts |
EndpointMetadata | Interface | endpointDecorator.ts |
ParameterType | Enum | parameterDecorator.ts |
ParameterMetadata | Interface | parameterDecorator.ts |
FunctionMetadata | Class | functionMetadata.ts |
RequestExecutor | Class | requestExecutor.ts |
ExecuteLifeCycle | Interface | executeLifeCycle.ts |
EndpointReturnType | Enum | endpointReturnTypeCapable.ts |
autoGeneratedError | Function | generated.ts |
buildRequestExecutor | Function | apiDecorator.ts |
getParameterNames | Function | reflection.ts |
Related Pages
- Fetcher (Core) - The HTTP client that decorators delegate to
- OpenAI - Real-world example using decorators with
ExecuteLifeCycle - Generator - Auto-generates decorator-based API classes from OpenAPI specs
- Wow - DDD/CQRS decorator-based clients
- Packages Overview - All packages in the ecosystem