fetcher-decorator-service · git:20260925.dadfae9 · 2026-09-25 · sha256 8799c9f422ee1ec3
fetcher-decorator-service git:20260925.dadfae9A
Immutable. This exact content never changes and is served at /api/v1/blob/8799c9f422ee1ec3.
---
name: fetcher-decorator-service
description: >
Declare typed API service classes with `@ahoo-wang/fetcher-decorator`: `@api`, `@get`/`@post`/`@put`/`@patch`/`@del`, `@path`/`@query`/`@header`/`@body`, per-endpoint result extractors and `ExecuteLifeCycle` hooks. Use when endpoints are stable and should read as a class. For client setup or interceptors use fetcher-integration; for React state use fetcher-react-hooks.
---
# fetcher-decorator-service
## Decisions
- **Decorator service vs direct calls**: use a class when endpoints are stable and discoverable; call the fetcher directly (`$fetcher-integration`) when URLs or bodies are built dynamically.
- **Which fetcher**: `@api(basePath, { fetcher: 'api' })` resolves a `NamedFetcher` by name at call time, falling back to `fetcherRegistrar.default`. An instance `apiMetadata` field overrides class options; assigning a new object takes effect on the next call (mutating it in place does not).
- **Cross-cutting behaviour** (auth, retry, recovery) stays in fetcher interceptors. `afterExecute` runs only after a successful exchange — with the default status validation a 401 throws before it.
## Gotchas a capable model gets wrong
- Legacy decorators: the app needs `experimentalDecorators: true` (not `emitDecoratorMetadata`); compiled as standard decorators, `@api` throws a TypeError saying so. `reflect-metadata` ships with the package and is imported by it; no entry-point import is needed.
- Method bodies are placeholders: `throw autoGeneratedError(...args)`. If that error is actually thrown at runtime, `@api` or the method decorator is missing.
- DELETE is `@del` (not `@delete`); there is no `@trace` — use `@endpoint(HttpMethod.TRACE, path)`.
- Decorated methods default to `JsonResultExtractor` and `EndpointReturnType.RESULT`, unlike core `fetcher.get()` which returns a `Response`. Use `{ returnType: EndpointReturnType.EXCHANGE }` or a `resultExtractor` per endpoint to change it.
- Parameter names are read from the function source when omitted, which minifiers break: always pass names (`@path('id')`). `null`/`undefined` arguments are skipped. A plain object passed to `@query`/`@path`/`@header` is spread into keys; an array, `Date` or other value is bound to the parameter name (`@query('ids') ids: number[]` → `ids=1&ids=2`; an array header is comma-joined).
- An `AbortSignal` or `AbortController` argument is detected automatically; it applies together with the fetcher timeout (whichever fires first aborts).
- A subclass inherits endpoints but uses its own `@api` base path — inherited paths must still resolve there. Overriding an inherited endpoint without an endpoint decorator keeps the override's own code.
- A method called without its instance (`const { getUser } = service`) throws a TypeError; bind it or call it on the service.
## Minimal example
```ts
import { NamedFetcher } from '@ahoo-wang/fetcher';
import {
api,
autoGeneratedError,
body,
get,
path,
post,
query,
} from '@ahoo-wang/fetcher-decorator';
new NamedFetcher('api', { baseURL: 'https://api.example.com' });
@api('/users', { fetcher: 'api' })
export class UserService {
@get('/{id}')
getUser(@path('id') id: string): Promise<User> {
throw autoGeneratedError(id);
}
@get('')
list(@query('page') page?: number): Promise<User[]> {
throw autoGeneratedError(page);
}
@post('')
create(@body() user: Omit<User, 'id'>): Promise<User> {
throw autoGeneratedError(user);
}
}
```
## References
- `references/api.md`: decorator signatures, parameter binding rules, execution flow, lifecycle hooks, inheritance and CRUD examples. Load it for anything beyond the example above.
## Related Skills
- $fetcher-integration: the `Fetcher`/`NamedFetcher` and interceptors underneath.
- $fetcher-llm-streaming: endpoints that return SSE streams.
- $fetcher-react-hooks: `createExecuteApiHooks` / `createQueryApiHooks` turn a service into hooks.