Skip to content

Decorators API

The @ahoo-wang/fetcher-decorator package provides a declarative way to define API service classes using TypeScript decorators. Methods decorated with HTTP verb decorators are automatically replaced with implementations that build and execute HTTP requests.

Requires reflect-metadata

You must import reflect-metadata before using any decorator:

typescript
import 'reflect-metadata';

Source: packages/decorator/src/index.ts

Class Decorator

@api

Defines a class as an API service with shared configuration.

typescript
@api(basePath?: string, metadata?: ApiMetadata)
ParameterTypeDefaultDescription
basePathstring''URL prefix for all endpoints in the class
metadataOmit<ApiMetadata, 'basePath'>{}Shared configuration for all methods

ApiMetadata Properties

PropertyTypeDescription
basePathstringURL prefix prepended to all endpoint paths
headersRequestHeadersDefault headers for all requests
timeoutnumberDefault timeout in milliseconds
fetcherstring | FetcherFetcher instance or name (default: 'default')
resultExtractorResultExtractor<any>Default result extractor
attributesRecord<string, any> | Map<string, any>Shared request attributes
returnTypeEndpointReturnTypeReturn type strategy ('Result' or 'Exchange')
urlParamsUrlParamsDefault URL parameters

Source: packages/decorator/src/apiDecorator.ts:40

Example

typescript
import 'reflect-metadata';
import { api, get, post, body, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';

@api('/api/v1', {
  headers: { 'Authorization': 'Bearer token' },
  timeout: 5000,
  fetcher: 'myFetcher',
})
class UserService {
  @get('/users')
  getUsers(): Promise<User[]> {
    throw autoGeneratedError();
  }

  @post('/users')
  createUser(@body() user: User): Promise<User> {
    throw autoGeneratedError();
  }
}

Source: packages/decorator/src/apiDecorator.ts:232

HTTP Method Decorators

All method decorators share the same signature:

typescript
@<method>(path?: string, metadata?: MethodEndpointMetadata)
DecoratorHTTP MethodDescription
@getGETRetrieve data from the server
@postPOSTCreate new resources
@putPUTReplace existing resources
@delDELETERemove resources
@patchPATCHPartially update resources
@headHEADRetrieve headers only
@optionsOPTIONSDescribe communication options

MethodEndpointMetadata

Extends ApiMetadata without method and basePath:

PropertyTypeDescription
headersRequestHeadersEndpoint-specific headers (merged with class headers)
timeoutnumberEndpoint-specific timeout
fetcherstring | FetcherEndpoint-specific fetcher
resultExtractorResultExtractor<any>Endpoint-specific result extractor
attributesRecord<string, any> | Map<string, any>Endpoint-specific attributes
returnTypeEndpointReturnTypeReturn type override
urlParamsUrlParamsEndpoint-specific URL parameters

Source: packages/decorator/src/endpointDecorator.ts:33

Generic @endpoint Decorator

For HTTP methods not covered by convenience decorators:

typescript
import { endpoint, HttpMethod } from '@ahoo-wang/fetcher-decorator';

@get('/users')
@endpoint(HttpMethod.TRACE, '/trace-endpoint')
traceEndpoint(): Promise<Response> {
  throw autoGeneratedError();
}

Source: packages/decorator/src/endpointDecorator.ts:59

Parameter Decorators

Parameter decorators specify how method arguments map to HTTP request components.

DecoratorParameterTypeDescription
@path(name?)PATHInserts value into URL path placeholder
@query(name?)QUERYAppends value as query string parameter
@header(name?)HEADERAdds value to request headers
@body()BODYSets the request body
@request()REQUESTPass a complete ParameterRequest object
@attribute(name?)ATTRIBUTEAdds value to exchange attributes

Source: packages/decorator/src/parameterDecorator.ts:199

Parameter Binding Rules

  1. Name is optional. If omitted, the parameter name is extracted from the TypeScript function signature via reflect-metadata:

    typescript
    @get('/users/{userId}')
    getUser(@path() userId: string) { throw autoGeneratedError(); }
  2. Object expansion. @path, @query, @header, and @attribute support plain objects. Each key-value pair is expanded into individual parameters:

    typescript
    @get('/users/{id}/posts/{postId}')
    getUserPost(@path() params: { id: string, postId: string }) { throw autoGeneratedError(); }
  3. AbortSignal / AbortController. If an argument is an AbortSignal or AbortController instance, it is automatically used for request cancellation -- no decorator needed.

Source: packages/decorator/src/functionMetadata.ts:224

@path

typescript
@get('/users/{id}/posts/{postId}')
getUserPost(
  @path('id') userId: string,
  @path('postId') postId: string,
): Promise<Post[]> {
  throw autoGeneratedError();
}

@query

typescript
@get('/users')
searchUsers(
  @query('limit') limit: number,
  @query('offset') offset: number,
): Promise<User[]> {
  throw autoGeneratedError();
}
typescript
@get('/users')
getUsers(@header('Authorization') token: string): Promise<User[]> {
  throw autoGeneratedError();
}

@body

typescript
@post('/users')
createUser(@body() user: CreateUserRequest): Promise<User> {
  throw autoGeneratedError();
}

@request

Pass a complete request configuration object:

typescript
interface ParameterRequest<BODY> extends FetchRequestInit<BODY>, PathCapable {}

@post('/users')
createUsers(@request() req: ParameterRequest): Promise<User[]> {
  throw autoGeneratedError();
}

// Usage:
await service.createUsers({
  path: '/custom-path',
  headers: { 'X-Custom': 'value' },
  body: [{ name: 'John' }],
  timeout: 10000,
});

@attribute

Pass attributes accessible by interceptors:

typescript
@get('/users/{id}')
getUser(
  @path('id') id: string,
  @attribute('requestId') requestId: string,
): Promise<User> {
  throw autoGeneratedError();
}

Source: packages/decorator/src/parameterDecorator.ts:408

autoGeneratedError

A placeholder error thrown inside decorated method bodies. The decorator replaces the method implementation at class decoration time, so the body is never executed. The autoGeneratedError function exists to satisfy ESLint and TypeScript requirements.

typescript
import { autoGeneratedError } from '@ahoo-wang/fetcher-decorator';

@get('/users')
getUsers(): Promise<User[]> {
  throw autoGeneratedError();
}

Source: packages/decorator/src/generated.ts:41

EndpointReturnType

Controls what decorated methods return.

ValueDescription
EndpointReturnType.RESULT (default)Returns the extracted result (e.g., parsed JSON)
EndpointReturnType.EXCHANGEReturns the full FetchExchange object
typescript
@api('/api', { returnType: EndpointReturnType.EXCHANGE })
class ExchangeApi {
  @get('/data')
  getData(): Promise<FetchExchange> {
    throw autoGeneratedError();
  }
}

Source: packages/decorator/src/endpointReturnTypeCapable.ts:14

ExecuteLifeCycle

Interface for hooking into the request execution lifecycle.

typescript
interface ExecuteLifeCycle {
  beforeExecute?(exchange: FetchExchange): void | Promise<void>;
  afterExecute?(exchange: FetchExchange): void | Promise<void>;
}

Implement this interface on your API class to add custom logic before and after interceptor processing:

typescript
@api('/api/v1')
class LoggingApi implements ExecuteLifeCycle {
  beforeExecute(exchange: FetchExchange) {
    console.log('Request:', exchange.request.url);
  }
  afterExecute(exchange: FetchExchange) {
    console.log('Response:', exchange.response?.status);
  }

  @get('/data')
  getData(): Promise<Data> {
    throw autoGeneratedError();
  }
}

Source: packages/decorator/src/executeLifeCycle.ts:23

Decorator Resolution Order

mermaid
flowchart TD
    A["Class decorated with @api"] --> B["Store ApiMetadata on constructor"]
    B --> C["Walk prototype chain"]
    C --> D{"Method has<br>@endpoint metadata?"}
    D -->|No| E["Skip method"]
    D -->|Yes| F["Collect ParameterMetadata<br>from @path, @query, etc."]
    F --> G["Create FunctionMetadata"]
    G --> H["Replace method with<br>async RequestExecutor.execute()"]
    H --> I["Method ready for use"]

    I --> J["service.getData(args)"]
    J --> K["RequestExecutor.execute(args)"]
    K --> L["resolveExchangeInit(args)"]
    L --> M["Map args to path/query/header/body<br>via ParameterMetadata"]
    M --> N["Build FetchRequest"]
    N --> O["fetcher.resolveExchange()"]
    O --> P["BeforeExecute lifecycle hook"]
    P --> Q["fetcher.interceptors.exchange()"]
    Q --> R["AfterExecute lifecycle hook"]
    R --> S["extractResult()"]
    S --> T["Return to caller"]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style J fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style K fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style O fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Q fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Metadata Priority

When the same property is defined at multiple levels, the resolution follows this priority (highest last):

mermaid
graph TD
    A["Class-level @api metadata"] --> D["Merged Metadata"]
    B["Instance apiMetadata property"] --> D
    C["Method-level @endpoint metadata"] --> D
    D --> E["RequestExecutor uses<br>merged metadata"]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style D fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style E fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Complete Example

typescript
import 'reflect-metadata';
import {
  api, get, post, put, del,
  path, query, header, body,
  autoGeneratedError, ExecuteLifeCycle
} from '@ahoo-wang/fetcher-decorator';
import type { FetchExchange } from '@ahoo-wang/fetcher';

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserListQuery {
  page: number;
  limit: number;
  keyword?: string;
}

@api('/api/v1/users', {
  headers: { 'Content-Type': 'application/json' },
  timeout: 10000,
})
class UserApi implements ExecuteLifeCycle {
  beforeExecute(exchange: FetchExchange) {
    console.log(`[UserApi] ${exchange.request.method} ${exchange.request.url}`);
  }

  @get('')
  listUsers(@query() query: UserListQuery): Promise<User[]> {
    throw autoGeneratedError();
  }

  @get('/{id}')
  getUser(@path('id') id: string): Promise<User> {
    throw autoGeneratedError();
  }

  @post('')
  createUser(@body() user: Omit<User, 'id'>): Promise<User> {
    throw autoGeneratedError();
  }

  @put('/{id}')
  updateUser(
    @path('id') id: string,
    @body() user: Partial<User>,
  ): Promise<User> {
    throw autoGeneratedError();
  }

  @del('/{id}')
  deleteUser(@path('id') id: string): Promise<void> {
    throw autoGeneratedError();
  }
}

// Usage:
const userApi = new UserApi();
const users = await userApi.listUsers({ page: 1, limit: 10, keyword: 'john' });
const user = await userApi.getUser('123');

Parameter Type Expansion

mermaid
sequenceDiagram
autonumber

    participant C as Caller
    participant MD as FunctionMetadata
    participant FE as FetchExchange

    C->>MD: resolveExchangeInit([userId, filters])
    MD->>MD: Process @path param<br>userId -> pathParams.id
    MD->>MD: Process @query param<br>filters object -> expand keys
    MD->>MD: Process @body param<br>body -> request.body
    MD->>MD: Process @header param<br>token -> headers.Authorization
    MD->>MD: Process @attribute param<br>metadata -> expand or set
    MD->>FE: Build FetchRequest with<br>resolved params, body, headers
    FE-->>C: Exchange with complete request

Released under the Apache License 2.0.