fetcher-decorator-service · diff
git:20260924.b36c6c8 to git:20260925.dadfae9
6 added, 5 removed. Audit A to A.
---
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, but is read once per method on its first call.
+ - **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`. `reflect-metadata` ships with the package and is imported by it; no entry-point import is needed.
+ - 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; object (and array) arguments to `@query`/`@path`/`@header` are spread into keys.
- - An `AbortSignal` or `AbortController` argument is detected automatically; a signal disables the fetcher timeout for that call.
- - A subclass inherits endpoints but uses its own `@api` base path — inherited paths must still resolve there.
+ - 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.